#!/bin/bash

#This script is the backbone of Pi-Apps. It hosts functions that all other scripts depend upon.
#By default, this file is hard to read. But there's an easy way to fix that. In your code editor, find the option to "Fold All".
#In Geany, this option can be found in the Document toolbar.
#Botspot has found it helpful to create a keyboard shortcut to do this quickly. You may find it helpful too.

#output functions below
error() { #red text and exit 1
  echo -e "\e[91m$1\e[0m" 1>&2
  exit 1
}

warning() { #yellow text
  echo -e "\e[93m\e[5m◢◣\e[25m WARNING: $1\e[0m" 1>&2
}

status() { #cyan text to indicate what is happening
  
  #detect if a flag was passed, and if so, pass it on to the echo command
  if [[ "$1" == '-'* ]] && [ ! -z "$2" ];then
    echo -e $1 "\e[96m$2\e[0m" 1>&2
  else
    echo -e "\e[96m$1\e[0m" 1>&2
  fi
}

status_green() { #announce the success of a major action
  echo -e "\e[92m$1\e[0m" 1>&2
}

debug() { #an echo command that only runs when debug mode is on
  [ "$pi_apps_debug" = true ] && echo "$1"
}

generate_logo() { #display colorized Pi-Apps logo in terminal
  #ANSI color codes: https://misc.flogisoft.com/bash/tip_colors_and_formatting
  #Search for unicode characters: https://unicode-search.net - search for "block" and "quadrant"
  
  # foreground colors
  blue1='\e[38;5;75m'
  blue2='\e[38;5;26m'
  blue3='\e[38;5;21m'
  blue4='\e[38;5;93m'
  
  green='\e[38;5;46m'
  darkgreen='\e[38;5;34m'
  red='\e[38;5;197m'
  white='\e[97m'
  black='\e[30m'
  default='\e[39m'
  # background colors
  bg_default='\e[49m'
  bg_black='\e[40m'
  bg_white='\e[107m'
  
  #complex logo requires Unicode 13 support (libicu66+)
  #this is available in Ubuntu 20.04+ and Debian 11+
  #use simpler logo when not supported to fix issue https://github.com/Botspot/pi-apps/issues/1441
  if [ -f /usr/lib/aarch64-linux-gnu/libicudata.so ]; then
    local version="$(readlink -f /usr/lib/aarch64-linux-gnu/libicudata.so | sed -n 's/.*libicudata.so.//p')"
  elif [ -f /usr/lib/arm-linux-gnueabihf/libicudata.so ]; then
    local version="$(readlink -f /usr/lib/arm-linux-gnueabihf/libicudata.so | sed -n 's/.*libicudata.so.//p')"
  elif find /usr/lib/aarch64-linux-gnu/libicudata.so.* &>/dev/null; then
    local version="$(find /usr/lib/aarch64-linux-gnu/libicudata.so.* | head -1 | sed -n 's+^/usr/lib/aarch64-linux-gnu/libicudata.so.++p')"
  elif find /usr/lib/arm-linux-gnueabihf/libicudata.so.* &>/dev/null; then
    local version="$(find /usr/lib/arm-linux-gnueabihf/libicudata.so.* | head -1 | sed -n 's+^/usr/lib/arm-linux-gnueabihf/libicudata.so.++p')"
  else
    local version="65"
  fi
  if (( $(echo "$version >= 66" | bc -l) )); then
    bg_black='\e[48;2;10;10;10m'
    echo -e "${bg_default}    \e[38;2;5;220;75m🭊\e[38;2;4;150;29m🬹🬹🬹\e[38;2;6;188;64m🬿${default}                                          ${darkgreen}                ${default}
 \e[38;2;83;213;255m🭈🬭\e[38;2;83;214;255m🬭\e[38;2;5;220;75m\e[48;2;83;212;255m🬎${bg_default}\e[38;2;84;201;251m🬭\e[38;2;84;190;248m🬭\e[38;2;85;178;244m🬭\e[38;2;6;188;64m\e[48;2;86;168;241m🬎${bg_default}\e[38;2;87;154;237m🬭🬭\e[38;2;87;136;231m🬽${default}                                       ${darkgreen}                ${default}
 \e[38;2;83;213;255m${bg_black}▋  \e[38;2;255;38;101m▄ \e[38;2;255;28;92m▄ \e[38;2;255;13;83m▄\e[38;2;89;114;225m  🮉${bg_default}${default}   █▀▀🭍 ▄    🭋🭡🭖🭀                      ${darkgreen}  ${black}    ${darkgreen}    ${black}    ${darkgreen}  ${default}
 \e[38;2;85;191;249m${bg_black}▋  \e[38;2;255;13;85m▄ \e[38;2;255;0;75m▄ \e[38;2;246;0;73m▄\e[38;2;90;83;215m  🮉${bg_default}${default}   █▄▄🭞 ▄ ${blue3}▄▄${default} 🭅▙▟🭐 █▀▀🭍 █▀▀🭍 🭂🬰🬰🬰       ${darkgreen}  ${black}    ${darkgreen}    ${black}    ${darkgreen}  ${default}
 \e[38;2;86;164;240m${bg_black}▋  \e[38;2;249;0;73m▄ \e[38;2;239;0;69m▄ \e[38;2;229;0;66m▄\e[38;2;92;58;207m  🮉${bg_default}${default}   █    █   🭋🭡  🭖🭀█▄▄🭞 █▄▄🭞 ▄▄▄🭞       ${darkgreen}      ${black}    ${darkgreen}      ${default}
 \e[38;2;87;137;232m🭕${bg_black}🭏\e[38;2;89;111;224m🬭\e[38;2;89;100;220m🬭\e[38;2;90;89;217m🬭\e[38;2;91;76;213m🬭\e[38;2;92;68;211m🬭\e[38;2;92;59;208m🬭\e[38;2;92;56;207m🬭🭄${bg_default}🭠${default}                  █    █               ${darkgreen}    ${black}        ${darkgreen}    ${default}
\e[0m                                                   ${darkgreen}    ${black}        ${darkgreen}    ${default}
                                                   ${darkgreen}    ${black}  ${darkgreen}    ${black}  ${darkgreen}    ${default}"
  else
    echo -e "${white}${bg_default}    ${green}▅${darkgreen}▅▅▅${green}▅${default}                                          ${darkgreen}                ${default}
 ${blue1}▂▂▂${green}\e[48;5;26m\e[7m▂\e[27m${bg_default}${blue2}▂▂▂${blue3}${green}\e[48;5;26m\e[7m▂\e[27m${bg_default}${blue3}▂▂▂${white}${default}                                       ${darkgreen}                ${default}
 ${bg_black}${blue1}▌  ${red}▄ ▄ ▄${blue3}  ▐${bg_default}${default}   █▀▀◣ ▄    ◢▀▀◣                      ${darkgreen}  ${black}    ${darkgreen}    ${black}    ${darkgreen}  ${default}
 ${bg_black}${blue2}▌  ${red}▄ ▄ ▄${blue3}  ▐${bg_default}${default}   █▄▄◤ ▄ ${blue3}▄▄${default} █▄▄█ █▀▀◣ █▀▀◣ ◢\e[7m━━━\e[27m       ${darkgreen}  ${black}    ${darkgreen}    ${black}    ${darkgreen}  ${default}
 ${bg_black}${blue2}▌  ${red}▄ ▄ ▄${blue4}  ▐${bg_default}${default}   █    █    █  █ █▄▄◤ █▄▄◤ ▄▄▄◤       ${darkgreen}      ${black}    ${darkgreen}      ${default}
 ${blue3}◥${bg_black}▃▃▃▃${blue4}▃▃▃▃▃${bg_default}◤${default}                  █    █               ${darkgreen}    ${black}        ${darkgreen}    ${default}
\e[0m                                                   ${darkgreen}    ${black}        ${darkgreen}    ${default}
                                                   ${darkgreen}    ${black}  ${darkgreen}    ${black}  ${darkgreen}    ${default}"
  fi
}
#end of output functions

add_english() { #add en_US locale for more accurate error
  if [ "$(cat /usr/share/i18n/SUPPORTED | grep -o 'en_US.UTF-8' )" == "en_US.UTF-8" ]; then 
    locale=$(locale -a | grep -oF 'en_US.utf8')
    if [ "$locale" != 'en_US.utf8' ]; then
      status "Adding en_US locale for better logging... "
      sudo sed -i '/en_US.UTF-8/s/^#[ ]//g' /etc/locale.gen
      sudo locale-gen
    fi
  else
    warning "en_US locale is not available on your system. This may cause bad logging experience."
  fi
  export LANG="en_US.UTF-8"
  export LANGUAGE="en_US.UTF-8"
  export LC_ALL="en_US.UTF-8"
}

#package functions
package_info() { #list everything dpkg knows about the $1 package. Note: the package has to be installed for this to show anything.
  local package="$1"
  [ -z "$package" ] && error "package_info(): no package specified!"
  #list lines in /var/lib/dpkg/status between the package name and the next empty line (empty line is then removed)
  sed -n -e '/^Package: '"$package"'$/,/^$/p' /var/lib/dpkg/status | head -n -1
  true #this may exit with code 141 if the pipe was closed early (to be expected with grep -v)
}

package_installed() { #exit 0 if $1 package is installed, otherwise exit 1
  local package="$1"
  [ -z "$package" ] && error "package_installed(): no package specified!"
  #find the package listed in /var/lib/dpkg/status
  #package_info "$package"
  
  #directly search /var/lib/dpkg/status
  grep -x "Package: $package" /var/lib/dpkg/status -A 2 | grep -qxF 'Status: install ok installed'
}

package_available() { #determine if the specified package-name exists in a local repository for the current dpkg architecture
  local package="$(awk -F: '{print $1}' <<<"$1")"
  local dpkg_arch="$(awk -F: '{print $2}' <<<"$2")"
  [ -z "$dpkg_arch" ] && dpkg_arch="$(dpkg --print-architecture)"
  [ -z "$package" ] && error "package_available(): no package name specified!"
  local output="$(apt-cache policy "$package":"$dpkg_arch" | grep "Candidate:")"
  if [ -z "$output" ]; then
    return 1
  elif echo "$output" | grep -q "Candidate: (none)"; then
    return 1
  else
    return 0
  fi
}

package_dependencies() { #outputs the list of dependencies for the $1 package
  local package="$1"
  [ -z "$package" ] && error "package_dependencies(): no package specified!"
  
  #find the package listed in /var/lib/dpkg/status
  package_info "$package" | grep '^Depends: ' | sed 's/^Depends: //g'
}

package_installed_version() { #returns the installed version of the specified package-name.
  local package="$1"
  [ -z "$package" ] && error "package_installed_version(): no package specified!"
  #find the package listed in /var/lib/dpkg/status
  package_info "$package" | grep '^Version: ' | awk '{print $2}'
}

package_latest_version() { #returns the latest available versions of the specified package-name. Doesn't matter if it's installed or not.
  local package="$1"
  [ -z "$package" ] && error "package_latest_version(): no package specified!"
  
  #support repo selection as '-t bookworm-backports' for $2 and $3
  local additional_flags
  additional_flags=()
  if [ "$2" == '-t' ];then
    additional_flags=(-t "$3")
  fi
  
  # use slower but more accurate apt list command to get package version for current architecture
  apt-cache policy "${additional_flags[@]}" "$package" 2>/dev/null | grep "Candidate: " | awk '{print $2}'
  #grep -rx "Package: $package" /var/lib/apt/lists --exclude="lock" --exclude-dir="partial" --after 4 | grep -o 'Version: .*' | awk '{print $2}' | sort -rV | head -n1
}

package_is_new_enough() { #check if the $1 package has an available version greater than or equal to $2
  local package="$1"
  [ -z "$package" ] && error "package_is_new_enough(): no package specified!"
  
  local compare_version="$2"
  [ -z "$package" ] && error "package_is_new_enough(): no comparison version number specified!"
  
  #determine the latest available version for the specified package
  local package_version="$(package_latest_version "$package")"
  
  #if version value not found, return 1 now
  if [ -z "$package_version" ];then
    return 1
  fi
  
  #given both the package_version and compare_version, see if the greater of the two is the available package's version
  if [ "$(echo "$package_version"$'\n'"$compare_version" | sort -rV | head -n1)" == "$package_version" ];then
    #if so, indicate success
    return 0
  else
    return 1
  fi
}

anything_installed_from_uri_suite_component() { #Given an apt repository uri, suite, and component, determine if any packages from it are currently installed
  local uri="$1"
  local suite="$2"
  component="$3" #can be left blank
  [ -z "$uri" ] && error "anything_installed_from_uri_suite_component: A repository uri must be specified."
  [ -z "$suite" ] && error "anything_installed_from_uri_suite_component: A repository suite must be specified."
  
  #find part of path to apt list file(s) to search for
  if [ -z "$component" ]; then
    local filepath="/var/lib/apt/lists/$(echo "$1" | sed 's+.*://++g' | sed "s,/$,," | tr '/' '_')_$(echo "$suite" | sed "s,/$,," | tr '/' '_')_"
  else
    local filepath="/var/lib/apt/lists/$(echo "$1" | sed 's+.*://++g' | sed "s,/$,," | tr '/' '_')_dists_$(echo "$suite" | sed "s,/$,," | tr '/' '_')_$(echo "$component" | sed "s,/$,," | tr '/' '_')_"
  fi
  debug $filepath
  
  #find all relevant package-lists
  local repofiles="$(ls ${filepath}*_Packages)"
  debug "$repofiles"
  
  #for every repo-file, check if any of them have an installed file
  local IFS=$'\n'
  local repofile
  local installed_packages="$(grep -xF 'Status: install ok installed' /var/lib/dpkg/status -B 2 | grep '^Package: ' | sed 's/^Package: //g' | sort)"
  for repofile in $repofiles ;do
    #search the repo-file for installed packages
    
    local packages_in_repo="$(grep '^Package: ' "$repofile" | awk '{print $2}' | sort)"
    local apt_cache_policy_output="$(echo "$packages_in_repo" | list_intersect "$installed_packages" | tr '\n' ' ' | xargs -r apt-cache policy)"
    
    #check if any installed packages also found on this repo are actually installed from this repo
    if [ -z "$component" ]; then
      echo "$apt_cache_policy_output" | grep -B1 "$(echo "$uri" | sed 's+.*://++g' | sed "s,/$,,") $suite" | awk '{print $1}' | grep -Fq '***' && return 0
    else
      echo "$apt_cache_policy_output" | grep -B1 "$(echo "$uri" | sed 's+.*://++g' | sed "s,/$,,") $suite/$component" | awk '{print $1}' | grep -Fq '***' && return 0
    fi
  done
  return 1
}

remove_repofile_if_unused() { #Given a sources.list.d file, delete it if nothing from that repository is currently installed. Deletion skipped if $2 is 'test'
  local file="$1"
  local testmode="$2"
  local key="$3"
  [ -z "$file" ] && error "remove_repo_if_unused: no sources.list.d file specified!"
  #return now if the list file does not exist
  [ -f "$file" ] || return 0

  #set default to not in use
  local in_use=0
  
  if [ "${file##*.}" == "list" ]; then
    # determine what uri, suite, and components are in a file
    local lines="$(cat "$file" | grep "^deb " | sed 's/^deb // ; s/\[.*\]//')"
    local IFS=$'\n'
    for line in $lines ;do
      local uri="$(echo "$line" | awk '{print $1}')"
      local suite="$(echo "$line" | awk '{print $2}')"
      local components="$(echo "$line" | awk '{$1=$2=""; print $0}')"
      local IFS=' '
      if [ -z "$components" ]; then
        debug "$uri $suite"
        if anything_installed_from_uri_suite_component "$uri" "$suite";then
          in_use=1
          break 1
        fi
      else
        for component in $components ;do
          debug "$uri $suite $component"
          if anything_installed_from_uri_suite_component "$uri" "$suite" "$component";then
            in_use=1
            break 2
          fi
        done
      fi
      local IFS=$'\n'
    done
  elif [ "${file##*.}" == "sources" ]; then
    #find empty lines (empty line, line with all spaces, or line with all tabs) in the file that separate stanzas. empty lines are not allowed between fields within a stanza
    #https://manpages.ubuntu.com/manpages/jammy/en/man5/deb822.5.html
    local empty_lines="$(grep -P -n '^$|^ +$|^\t+$' "$file" | awk '{print $1}' | sed 's/:*//g')"
    #get number of lines in file
    local num_lines=$(wc -l "$file" | awk '{print $1}')

    #always add last line to empty lines
    if [ -z "$empty_lines" ]; then
      empty_lines="$num_lines"
    else
      empty_lines+=$'\n'"$num_lines"
    fi
    debug "$empty_lines"

    #parse each stanza, starting at line 1
    local IFS=$'\n'
    local line_start=1
    for line_end in $empty_lines ;do
      # if Enabled: no, continue to next loop iteration
      sed -n "$line_start","$line_end"p  "$file" | grep -q '^Enabled: no' && continue
      # determine what uri, suite, and components are in a file
      #each stanza can only have one matching section. if there are multiple matches the last one is used
      #case should be ignored for fields
      local uris="$(sed -n "$line_start","$line_end"p  "$file" | grep -v '^#' | grep -i '^URIs:' | sed 's/URIs://Ig' | awk '{$1=$1};1' | tail -1)"
      local suites="$(sed -n "$line_start","$line_end"p "$file" | grep -v '^#' | grep -i '^Suites:' | sed 's/Suites://Ig' | awk '{$1=$1};1' | tail -1)"
      local components="$(sed -n "$line_start","$line_end"p "$file" | grep -v '^#' | grep -i '^Components:' | sed 's/Components://Ig' | awk '{$1=$1};1' | tail -1)"
      local IFS=' '
      for uri in $uris ;do
        for suite in $suites ;do
          if [ -z "$components" ]; then
            debug "$uri $suite"
            if anything_installed_from_uri_suite_component "$uri" "$suite";then
              in_use=1
              break 3
            fi
          else
            for component in $components ;do
              debug "$uri $suite $component"
              if anything_installed_from_uri_suite_component "$uri" "$suite" "$component";then
                in_use=1
                break 4
              fi
            done
          fi
        done
      done
      local IFS=$'\n'
      line_start="$line_end"
    done
  else
    error "$file was not of apt list or sources type"
  fi
  
  if [ "$testmode" == test ] && [ "$in_use" == 0 ];then
    echo "The given repository is not in use and can be deleted:"$'\n'"$file" 1>&2
  elif [ "$testmode" == test ];then
    echo "At least one package is preventing the repo from being removed"
  elif [ "$in_use" == 0 ];then
    status "Removing the $(basename "$file" | sed 's/.list$//g' | sed 's/.sources$//g') repo as it is not being used"
    sudo rm -f "$file"
    [ -f "$key" ] && sudo rm -f "$key" || true
  fi
  
}

#apt functions
apt_lock_wait() { #Wait until other apt processes are finished before proceeding
  #make sure english locale is added first
  add_english

  #check if sudo needs a password currently. this prevents sudo asking for a password in the below fuser command which would have "Waiting until APT locks are released... " written after it after 5 seconds
  #the result would look like something in the terminal to a user "[sudo] password for USER: Waiting until APT locks are released... " which might be confusing
  #this is very often the first command that pi-apps scripts run that requires sudo privileges from the user
  #passwordless sudo (like on piOS) will always skip the contents of the if
  if ! sudo -n true ; then
    # sudo needs a password so prompt the user to give one before running other commands
    sudo echo > /dev/null
  fi

  #in a background subprocess, after 5 seconds, say "Waiting until APT locks are released... "
  (sleep 5; echo -n "Waiting until APT locks are released... ") &
  local pid=$!
  
  while [ ! -z "$(sudo fuser /var/lib/dpkg/lock /var/lib/apt/lists/lock /var/cache/apt/archives/lock /var/log/unattended-upgrades/unattended-upgrades.log /var/lib/dpkg/lock-frontend /var/cache/debconf/config.dat 2>/dev/null)" ];do
    sleep 1
  done
  
  #Try to install a non-existent package to see if apt fails due to a lock-file. Repeat until no errors mention 'Could not get lock'
  #NOTE: after apt 3.0 this output may change. Refer to git blame for this line for more info.
  while sudo -E apt install lkqecjhxwqekc 2>&1 | grep -q 'Could not get lock' ;do
    sleep 1
  done
  
  #If the background process finished, then that means the "waiting until" message was displayed. This means the kill command will return 1, so echo Done
  kill $pid &>/dev/null || echo "Done"
}

less_apt() { #remove unwanted lines from apt output
  grep --line-buffered -v "apt does not have a stable CLI interface.\|Reading package lists...\|Building dependency tree\|Reading state information...\|Need to get\|Selecting previously unselected package\|Preparing to unpack\|Setting up \|Processing triggers for \|^$"
}

apt_update() { #run an apt update with error-checking and minimal output
  apt_lock_wait
  
  status "Running \e[7msudo apt update\e[27m..."
  set -o pipefail
  local output="$(sudo -E apt update --allow-releaseinfo-change "$@" 2>&1 | less_apt | tee /dev/stderr)"
  local exitcode=$?
  status "apt update complete."
  
  #inform user about autoremovable packages
  if [ ! -z "$(echo "$output" | grep 'autoremove to remove them' )" ];then
    echo -e "\e[33mSome packages are unnecessary.\e[39m Please consider running \e[4msudo a\e[0mp\e[4mt autoremove\e[0m."
  fi
  
  #inform user packages are upgradeable
  if [ ! -z "$(echo "$output" | grep 'packages can be upgraded' )" ];then
    echo -e "\e[33mSome packages can be upgraded.\e[39m Please consider running \e[4msudo a\e[0mp\e[4mt full-u\e[0mpg\e[4mrade\e[0m."
  elif [ ! -z "$(echo "$output" | grep 'package can be upgraded' )" ];then
    echo -e "\e[33mOne package can be upgraded.\e[39m Please consider running \e[4msudo a\e[0mp\e[4mt full-u\e[0mpg\e[4mrade\e[0m."
  fi
  
  #exit on apt error
  local errors="$(echo "$output" | grep '^[(E)|(Err]:')"
  if [ $exitcode != 0 ] || [ ! -z "$errors" ];then
    echo -e "\e[91mFailed to run \e[4msudo apt update\e[0m\e[39m!"
    echo -e "APT reported these errors:\n\e[91m$errors\e[39m"
    
    #run some apt error diagnosis
    echo "$output"
    exit 1
  fi
  return 0
}

repo_add() { #add local packages to the /tmp/pi-apps-local-packages repository
  #given local deb file(s), make a local apt repository in /tmp/pi-apps-local-packages
  
  #see: https://unix.stackexchange.com/questions/87130/how-to-quickly-create-a-local-apt-repository-for-random-packages-using-a-debian
  #and: https://serverfault.com/questions/447457/use-apt-get-source-on-a-debian-repo-without-using-etc-apt-source-list
  #and: https://askubuntu.com/questions/382664/use-custom-directory-for-apt-get
  
  #use this flag for apt commands to use the local repository: -o Dir::Etc::SourceList=/tmp/pi-apps-local-packages/source.list
  
  #ensure the repo-folder exists
  mkdir -p /tmp/pi-apps-local-packages || error "repo_add(): failed to create folder /tmp/pi-apps-local-packages"
  
  #move every mentioned deb file to it
  for file in "$@"; do
    mv -f "$file" /tmp/pi-apps-local-packages || error "repo_add(): failed to move '$file' to the repository: /tmp/pi-apps-local-packages"
  done
}

repo_refresh() { #index the pi-apps local apt repository
  
  [ -d /tmp/pi-apps-local-packages ] || error "repo_update(): cannot index the repository - it's missing! /tmp/pi-apps-local-packages"
  
  #index the repository by creating a Packages file
  (cd /tmp/pi-apps-local-packages && apt-ftparchive packages . > Packages) || error "repo_update(): apt-ftparchive failed to index the repository: /tmp/pi-apps-local-packages
The Pi-Apps developers have been receiving a few of these errors recently, but we can't figure out what the problem is without your help. Could you please reach out so we can solve this?"
  #Make sure the Packages file actually exists
  [ -f /tmp/pi-apps-local-packages/Packages ] || error "repo_update(): apt-ftparchive failed to index the repository: /tmp/pi-apps-local-packages
The Pi-Apps developers have been receiving a few of these errors recently, but we can't figure out what the problem is without your help. Could you please reach out so we can solve this?"
  
  #by default, apt-ftparchive will generate lines like "Filename: ./package-name". This seemed to have caused one error-report, so we remove the "./" to hopefully solve the problem.
  sed -i 's+^Filename: \./+Filename: +g' /tmp/pi-apps-local-packages/Packages
  
  # set repo origin name to pi-apps-local-packages - see PR #1986
  echo 'APT::FTPArchive::Release {
Origin "pi-apps-local-packages";
};' > /tmp/pi-apps-local-packages/aptftp.conf
  
  #hash the repository by creating a Release file
  (cd /tmp/pi-apps-local-packages && apt-ftparchive -c=/tmp/pi-apps-local-packages/aptftp.conf release . > Release) || error "repo_update(): apt-ftparchive failed to hash the repository: /tmp/pi-apps-local-packages
The Pi-Apps developers have been receiving a few of these errors recently, but we can't figure out what the problem is without your help. Could you please reach out so we can solve this?"
  
  #create a source.list for the repository - add pi-apps-local-packages to the top, then add lines from /etc/apt/sources.list
  rm -f /tmp/pi-apps-local-packages/source.list
  echo "deb [trusted=yes] file:/tmp/pi-apps-local-packages/ ./" > /tmp/pi-apps-local-packages/source.list
  cat /etc/apt/sources.list >> /tmp/pi-apps-local-packages/source.list
  
  # remove pi-apps-local-packages priority if it exists
  [ -f /etc/apt/preferences.d/pi-apps-local-packages ] && sudo rm -f /etc/apt/preferences.d/pi-apps-local-packages
  true
}

repo_rm() { #remove the local apt repository
  #wait for other operations to finish before continuing - hopefully this will solve cases when the pi-apps local repository was removed unexpectedly by a second process
  apt_lock_wait
  
  rm -rf /tmp/pi-apps-local-packages || sudo rm -rf /tmp/pi-apps-local-packages || error "repo_rm(): failed to remove the local repository: /tmp/pi-apps-local-packages"
  
  #Also remove broken symbolic link to /tmp/pi-apps-local-packages/./Packages
  sudo rm -f /var/lib/apt/lists/_tmp_pi-apps-local-packages_._Packages
}

app_to_pkgname() { #given an app-name, convert it to a unique, valid package-name that starts with 'pi-apps-'
  local app="$1"
  [ -z "$app" ] && error "app_to_pkgname(): no app-name specified"
  
  echo "pi-apps-$(echo "$app" | md5sum | cut -c1-8 | awk '{print $1}')"
}

install_packages() { #Make some packages dependencies of the $app app. Package-names, regex, filenames, and urls are supported.
  
  #array-variable to store custom apt options (for local repositories)
  local apt_flags=()
  
  #convert input array to newline-separated string
  local IFS=' '
  local repo_selection='' #store selected repo to install from, eg. bookworm-backports
  while [ $# -gt 0 ]; do
    #pass through -t args to apt: for "-t bookworm-backports"
    if [ "$1" == '-t' ];then
      repo_selection="$2"
      apt_flags+=('-t' "$2")
      shift
      shift
    else
      local packages+="$1
"
      shift
    fi
  done
  packages="${packages::-1}" #remove final empty newline
  
  #the $app variable must contain something
  [ -z "$app" ] && error 'install_packages function can only be used by apps to install packages. (the $app variable was not set)'
  
  status "Will install these packages: $(tr '\n' ' ' <<<"$packages")"
  
  #variable to remember if the pi-apps-local-packages repository is being used
  local using_local_packages=0
  repo_rm #remove the local repo, just in case the last operation left it in an unrecoverable state.
  
  #handle regex, urls, local packages
  IFS=$'\n'
  local package
  for package in $packages ;do
    
    #handle local packages (package-name starts with /)
    if [[ "$package" == /* ]];then
      
      #status "Handling local package $package"
      
      [ -f "$package" ] || error "install_packages(): Local package does not exist! ($package)"
      
      #determine the package name, package version, and architecture from the file
      local dpkg_deb_output="$(dpkg-deb -I "$package")"
      local packagename="$(echo "$dpkg_deb_output" | grep "^ Package:" | awk '{print $2}')"
      local packageversion="$(echo "$dpkg_deb_output" | grep "^ Version:" | awk '{print $2}')"
      local packagearch="$(echo "$dpkg_deb_output" | grep "^ Architecture:" | awk '{print $2}')"
      [ -z "$packagename" ] && error "install_packages(): failed to determine a package-name for the file '$package'"
      [ -z "$packageversion" ] && error "install_packages(): failed to determine a package-version for the file '$package'"
      [ -z "$packagearch" ] && error "install_packages(): failed to determine a package-architecture for the file '$package'"
      unset dpkg_deb_output
      
      #foreign arch: add :armhf or :arm64 to the packagename if this local package is of a foreign architecture
      if [ "$packagearch" != "$(dpkg --print-architecture)" ] && [ "$packagearch" != "all" ];then
        packagename+=":$packagearch"
      fi
      
      #add this local package to the pi-apps-local-packages repository
      repo_add "$package" || return 1
      using_local_packages=1 #remember that the pi-apps-local-packages repository is being used
      
      #replace package filename with name of package
      packages="$(echo "$packages" | sed "s|$package|$packagename (>= $packageversion)|")"
      
    #handle urls
    elif [[ "$package" == *://* ]];then
      
      #status "Handling url: $package"
      
      local filename="/tmp/$(basename "$(echo "$package" | sed 's+/download$++g')")"
      
      #add .deb extension if filename doesn't end with it.
      if [ "${filename: -4}" != ".deb" ]; then
        status "$filename is not ending with .deb, renaming it to '${filename}.deb'..." 
        local filename="${filename}.deb"
      fi
      
      for i in {1..3}; do #download retry loop
        wget -O "$filename" "$package" || rm -f "$filename"
        
        if [ -f "$filename" ];then
          break
        fi
        warning "Package download failed. (Attempt $i of 3)"
      done
      [ -f "$filename" ] || error "install_packages(): Downloaded package does not exist! ($filename)"
      
      #determine the package name, package version, and architecture from the file
      local dpkg_deb_output="$(dpkg-deb -I "$filename")"
      local packagename="$(echo "$dpkg_deb_output" | grep "^ Package:" | awk '{print $2}')"
      local packageversion="$(echo "$dpkg_deb_output" | grep "^ Version:" | awk '{print $2}')"
      local packagearch="$(echo "$dpkg_deb_output" | grep "^ Architecture:" | awk '{print $2}')"
      [ -z "$packagename" ] && error "install_packages(): failed to determine a package-name for the file '$filename'"
      [ -z "$packageversion" ] && error "install_packages(): failed to determine a package-version for the file '$filename'"
      [ -z "$packagearch" ] && error "install_packages(): failed to determine a package-architecture for the file '$filename'"
      unset dpkg_deb_output
      
      #foreign arch: add :armhf or :arm64 to the packagename if this local package is of a foreign architecture
      if [ "$packagearch" != "$(dpkg --print-architecture)" ] && [ "$packagearch" != "all" ];then
        packagename+=":$packagearch"
      fi
      
      #add this local package to the pi-apps-local-packages repository
      repo_add "$filename" || return 1
      using_local_packages=1 #remember that the pi-apps-local-packages repository is being used
      
      #replace package url with name of package
      packages="$(echo "$packages" | sed "s|^${package}$|$packagename (>= $packageversion)|")"
      
    #expand regex (package-name contains *)
    elif echo "$package" | grep -q '*' ;then 
      
      status "Expanding regex in '${package}'..."
      
      list="$(apt-cache search "$package" | awk '{print $1}' | grep "$(echo "$package" | tr -d '*')")"
      
      #replace package with expanded list
      packages="$(echo "$packages" | grep -vF "$package")"$'\n'"$list"
    
    #request package version if backports repo was specified
    elif [ ! -z "$repo_selection" ];then
      local packageversion="$(package_latest_version "$package" -t "$repo_selection" | sed 's/+.*//g')"
      if [ -z "$packageversion" ];then
        apt_update "${apt_flags[@]}" || exit 1
        packageversion="$(package_latest_version "$package" -t "$repo_selection" | sed 's/+.*//g')"
      fi
      
      if [ -z "$packageversion" ];then
        error "install_packages(): failed to get package version for $package, apt-cache output was $(apt-cache policy -t "$repo_selection" "$package" 2>&1)"
      fi
      #add version specification to package
      packages="$(echo "$packages" | sed "s|^${package}$|$package (>= $packageversion)|")"
    fi
  done
  #now package list shouldn't contain any '*' characters, urls, local filepaths
  if echo "$packages" | grep -q '*';then 
    error "install_packages(): failed to remove all regex from the package list:\n$packages"
  elif [[ "$packages" == *://* ]];then
    error "install_packages(): failed to remove all urls from the package list:\n$packages"
  elif [[ "$packages" == */* ]];then
    error "install_packages(): failed to remove all filenames from the package list:\n$packages"
  fi #package list contains no '*' characters, urls, local filepaths
  
  #change the $packages list from newline-delimited to comma-delimited
  packages="$(tr '\n' ',' <<<"$packages")"
  
  if [ "$using_local_packages" == 1 ];then
    #Initialize the pi-apps-local-packages repository
    repo_refresh || return 1
    #add this repository to flags to add to apt
    apt_flags+=(-o 'Dir::Etc::SourceList=/tmp/pi-apps-local-packages/source.list')
  fi
  
  status "Creating an empty apt-package to install the necessary apt packages..."
  
  #to avoid issues with symbols and spaces in app names, we shasum the app name for use in apt
  local package_name="$(app_to_pkgname "$app")"
  
  echo "It will be named: $package_name"
  
  #If this app's dummy deb is already installed, add its dependencies to the list.
  #this allows install_packages() to be used multiple times in an app's script
  if package_installed "$package_name" ;then
    
    local existing_deps="$(package_dependencies "$package_name")"
    status "The $package_name package is already installed. Inheriting its dependencies: $(echo "$existing_deps")"
    packages+=",""$(echo "$existing_deps" | sed 's/, /,/g')"
  fi
  
  { #create dummy apt package that depends on the packages this app requires
    
    #this stores the comma-separated list of dependency packages. It removes duplicate entries.
    local depends="$(echo "$packages" | sed 's/,|/ |/g' | sed 's/|,/| /g' | tr ',' '\n' | sort -u -t' ' -k1,1 | tr '\n' ',' | sed 's/^,//g' | sed 's/,$//g' | sed 's/,/, /g' ; echo)"
    
    rm -rf /tmp/$package_name /tmp/$package_name.deb
    mkdir -p /tmp/$package_name/DEBIAN
    echo "Maintainer: Pi-Apps team
Name: $app
Description: Dummy package created by pi-apps to install dependencies for the '$app' app
Version: 1.0
Architecture: all
Priority: optional
Section: custom
Depends: $depends
Package: $package_name" > /tmp/$package_name/DEBIAN/control
    
    #fix error report "dpkg-deb: error: control directory has bad permissions 700 (must be >=0755 and <=0775)"
    #The two zeros fix error report "dpkg-deb: error: control directory has bad permissions 2755 (must be >=0755 and <=0775)"
    sudo chmod -R '00755' /tmp/$package_name
    
    #display the finished "Depends: " line to the user
    grep --color=never "^Depends: " /tmp/$package_name/DEBIAN/control
  }
  
  #Skip installing the dummy deb if it is already installed and has an identical control file
  if package_installed "$package_name" && [ "$(package_info "$package_name" | sort | grep -v '^Status: ')" == "$(cat /tmp/$package_name/DEBIAN/control | sort)" ];then
    echo "$package_name is already installed and no changes would be made. Skipping..."
  else
    
    #Build .deb file for the dummy package
    local output="$(dpkg-deb --build /tmp/$package_name 2>&1)"
    if [ $? != 0 ] || [ ! -f /tmp/$package_name.deb ];then
      echo ""
      echo "$output"
      error "install_packages(): failed to create dummy deb ${package_name}!"
    fi
    
    #Before apt update, check if local repo still exists
    if [ "$using_local_packages" == 1 ] && [ ! -f /tmp/pi-apps-local-packages/Packages ];then
      error "User error: Uh-oh, the /tmp/pi-apps-local-packages folder went missing while installing packages.\nThis usually happens if you try to install several apps at the same time in multiple terminals."
    fi
    
    local i=0
    while true;do #retry loop ; run apt update and apt install again if apt's records of the local repo goes missing
      #run an apt update
      apt_update "${apt_flags[@]}" || exit 1
      
      #install dummy deb
      status "Installing the $package_name package..."
      
      apt_lock_wait
      set -o pipefail
      local output="$(sudo -E apt install -fy --no-install-recommends --allow-downgrades "${apt_flags[@]}" /tmp/$package_name.deb 2>&1 | less_apt | tee /dev/stderr)"
      local exitcode=$?
      status "Apt finished."
      
      if [ "$using_local_packages" == 1 ] && [ ! -f /var/lib/apt/lists/_tmp_pi-apps-local-packages_._Packages ] && [ $i != 5 ];then
        #another apt update process deleted apt's knowledge of the pi-apps local repo. Warn the user and try again, up to 5 tries.
        i=$((i+1))
        warning "Local packages failed to install because another apt update process erased apt's knowledge of the pi-apps local repository.\nTrying again... (attempt $i of 5)"
      else
        #apt may have succeeded or failed, but exit retry loop for Other Apt Update
        break
      fi
    done
    
    local errors="$(echo "$output" | grep '^[(E)|(Err]:')"
    if [ -z "$errors" ] && [ "$exitcode" != 0 ]; then
      echo -e "\e[91mFailed to install the packages!\e[39m"
      echo "User error: Apt exited with a failed exitcode ($exitcode) and no error (E/Err) output. This could indicate system corruption (eg: storage corruption or unstable overclocking)."
      exit 1
    elif [ ! -z "$errors" ];then
      echo -e "\e[91mFailed to install the packages!\e[39m"
      echo -e "The APT reported these errors:\n\e[91m$errors\e[39m"      
      #some error reports seem to indicate that package URLs aren't being properly downloaded. This output aims to solve the mystery.
      if [ "$using_local_packages" == 1 ] && [ ! -f /tmp/pi-apps-local-packages/Packages ];then
        echo "User error: Uh-oh, the /tmp/pi-apps-local-packages folder went missing while installing packages.\nThis usually happens if you try to install several apps at the same time in multiple terminals."
      elif [ "$using_local_packages" == 1 ] && ( echo "$output" | grep -q 'but it is not installable' || echo "$output" | grep -q 'but it is not going to be installed' || echo "$output" | grep -q "but .* is to be installed" ) ;then
        echo -e "\e[91mThe Pi-Apps Local Repository was being used, and a package seemed to not be available. Here's the Packages file:\e[39m"
        cat /tmp/pi-apps-local-packages/Packages
        echo -e "Attempting apt --dry-run installation of the problematic package(s) for debugging purposes:\n"
        ( echo "$output" | grep 'but it is not installable' || echo "$output" | grep 'but it is not going to be installed' || echo "$output" | grep "but .* is to be installed" ) | awk '{print $4}' | uniq | xargs sudo -E apt-get install -fy --no-install-recommends --allow-downgrades --dry-run "${apt_flags[@]}"
        echo -e "Printing apt-cache policy output for debugging purposes:\n"
        apt-cache policy
      fi
      
      exit 1
    fi
  fi
  
  rm -f /tmp/$package_name.deb
  rm -rf /tmp/$package_name
  
  #delete the local repository if it was used
  if [ "$using_local_packages" == 1 ];then
    repo_rm
  fi
  
  status_green "Package installation complete."
}

purge_packages() { #Allow dependencies of the $app app to be autoremoved.
  #the $app variable must contain something
  [ -z "$app" ] && error 'purge_packages function can only be used by apps to install packages. (the $app variable was not set)'
  
  status "Allowing packages required by the $app app to be uninstalled"
  
  #to avoid issues with symbols and spaces in app names, we shasum the app name for use in apt
  local package_name="$(app_to_pkgname "$app")"
  
  #if dummy deb found/installed
  if package_installed "$package_name" ;then
    echo "These packages were: $(package_dependencies "$package_name")"
    
    status "Purging the $package_name package..."

    if [ "$script_input" == "update" ]; then
      # skip --autoremove for faster updates. this prevents dummy deb dependencies from needing to be uninstalled and reinstalled on updates
      apt_lock_wait
      local output="$(sudo -E apt purge -y "$package_name" 2>&1 | less_apt | tee /dev/stderr)"
      status "Apt finished."
    else
      apt_lock_wait
      local output="$(sudo -E apt purge -y "$package_name" --autoremove 2>&1 | less_apt | tee /dev/stderr)"
      status "Apt finished."
    fi
    
    errors="$(echo "$output" | grep '^[(E)|(Err]:')"
    if [ ! -z "$errors" ];then
      echo -e "\e[91mFailed to uninstall the packages!\e[39m"
      echo -e "APT reported these errors:\n\e[91m$errors\e[39m"
      #run some apt error diagnosis
      echo "$output"
      exit 1
    fi
    
  elif [ -f "${DIRECTORY}/data/installed-packages/${app}" ];then
    #legacy pkg-install implementation
    warning "Using the old implementation - an installed-packages file instead of a dummy deb"
    
    local packages="$(cat "${DIRECTORY}/data/installed-packages/${app}" | tr '\n' ' ' | sed 's/  / /g')"
    
    #normal mode
    local output="$(sudo -E apt purge -y $packages 2>&1)"
    exitcode=$?
    
    errors="$(echo "$output" | grep '^[(E)|(Err]:')"
    if [ $exitcode != 0 ] || [ ! -z "$errors" ];then
      echo -e "\e[91mFailed to uninstall the packages!\e[39m"
      echo -e "APT reported these errors:\n\e[91m$errors\e[39m"
      #run some apt error diagnosis
      echo "$output"
      exit 1
    fi
  else
    status "The $package_name package is not installed so there's nothing to do."
  fi
  
  status_green "All packages have been purged successfully."
  rm -f "${DIRECTORY}/data/installed-packages/${app}"
}

get_icon_from_package() { #given a package-name, find all png files that it installed and print the one with the largest file-size.
  [ -z "$1" ] && error "get_icon_from_package(): requires an apt package name"
  
  #Find dependencies of the listed packages and scan them too
  local package=''
  local extra_packages=''
  for package in "$@" ;do
    #for every package specified, look for dependencies to that package that begin with the same name as the original package
    #Example: given the 'shotwell' package, this will find the 'shotwell-common' package, as well as others
    extra_packages+=" $(package_dependencies "$package" | sed 's/, \||/\n/g' | awk '{print $1}' | grep "^$package" | sort | uniq | tr '\n' ' ')"
  done
  dpkg-query -L "$@" $extra_packages 2>/dev/null | grep '\.png$\|\.svg$' | grep '/icons/\|/pixmaps/' | grep -v /symbolic/ | xargs wc -c | grep -v ' total' | sort -nr | head -n1 | sed 's/  / /g' | sed 's/^ //g' | tr ' ' '\n' | tail -n +2
}

ubuntu_ppa_installer() { #setup a PPA on an Ubuntu distro. Arguments: ppa_name
  local ppa_name="$1"
  [ -z "$1" ] && error "ubuntu_ppa_installer(): This function is used to add a ppa to a ubuntu based install but a required input argument was missing."
  local ppa_grep="$ppa_name"
  [[ "${ppa_name}" != */ ]] && local ppa_grep="${ppa_name}/"
  local ppa_added=$(apt-get indextargets --no-release-info --format '$(SITE) $(RELEASE) $(TARGET_OF)' | sort -u | awk '{if ($3=="deb") print $1" "$2 }' | grep "$ppa_grep" | wc -l)
  if [[ $ppa_added -eq "1" ]]; then
    status "Skipping $ppa_name PPA, already added"
  else
    status "Adding $ppa_name PPA"
    sudo add-apt-repository "ppa:$ppa_name" -y || exit 1
    apt_update || exit 1
  fi
  # check if ppa .list filename does not exist under the current distro codename
  # on a distro upgrade the .list filename is not updated and add-apt-repository can re-use the old filename
  local ppa_dist="$__os_codename"
  local standard_filename="/etc/apt/sources.list.d/${ppa_name%/*}-ubuntu-${ppa_name#*/}-${ppa_dist}.list" 
  if [[ ! -f "$standard_filename" ]] && ls /etc/apt/sources.list.d/${ppa_name%/*}-ubuntu-${ppa_name#*/}-*.list 1> /dev/null; then
    local original_filename="$(ls /etc/apt/sources.list.d/${ppa_name%/*}-ubuntu-${ppa_name#*/}-*.list | head -1)"
    # change the filename to match the current distro codename
    sudo mv "$original_filename" "$standard_filename"
    sudo rm -f "$original_filename".distUpgrade
    sudo rm -f "$original_filename".save
  fi
}

debian_ppa_installer() { #setup a PPA on a Debian distro. Arguments: ppa_name distribution key
  local ppa_name="$1"
  local ppa_dist="$2"
  local key="$3"
  [ -z "$1" ] || [ -z "$2" ] || [ -z "$3" ] && error "debian_ppa_installer(): This function is used to add a ppa to a debian based install but a required input argument was missing."
  local ppa_grep="$ppa_name"
  [[ "${ppa_name}" != */ ]] && local ppa_grep="${ppa_name}/ubuntu ${ppa_dist}"
  local ppa_added=$(apt-get indextargets --no-release-info --format '$(SITE) $(RELEASE) $(TARGET_OF)' | sort -u | awk '{if ($3=="deb") print $1" "$2 }' | grep "$ppa_grep" | wc -l)
  if [[ $ppa_added -eq "1" ]]; then
    status "Skipping $ppa_name PPA, already added"
  else
    status "Adding $ppa_name PPA"
    echo "deb https://ppa.launchpadcontent.net/${ppa_name}/ubuntu ${ppa_dist} main" | sudo tee /etc/apt/sources.list.d/${ppa_name%/*}-ubuntu-${ppa_name#*/}-${ppa_dist}.list || error "Failed to add repository to sources.list!"
    sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys "$key"
    if [ $? != 0 ];then
      sudo rm -f /etc/apt/sources.list.d/${ppa_name%/*}-ubuntu-${ppa_name#*/}-${ppa_dist}.list
      error "Failed to sign the $ppa_name PPA!"
    fi
    apt_update || exit 1
  fi
}

add_external_repo() { # add an external apt repo and its gpg key.
  # follows https://wiki.debian.org/DebianRepository/UseThirdParty specification with deb822 format https://repolib.readthedocs.io/en/latest/deb822-format.html
  # required inputs
  local reponame="$1"
  local pubkeyurl="$2"
  local uris="$3"
  local suites="$4"
  # potentially optional inputs
  # components is not used when suite is an absolute path
  local components="$5"
  # additional options can be specified as the 6th, 7th, 8th, etc argument (eg: "Architectures: arm64")

  # check if all needed vars are set  
  [ -z "$reponame" ] && error "add_external_repo: reponame not set"
  [ -z "$uris" ] && error "add_external_repo: uris not set"
  [ -z "$suites" ] && error "add_external_repo: suites not set"
  [ -z "$pubkeyurl" ] && error "add_external_repo: pubkeyurl not set"

  # exit if reponame or uri or suite contains space
  if [[ $reponame = *" "* ]] || [[ $uris = *" "* ]] || [[ $suites = *" "* ]]; then
    error "add_external_repo: provided reponame contains a space."
  fi

  # check if links are valid
  status "add_external_repo: checking 3rd party pubkeyurl validity"
  wget --spider "$pubkeyurl" || error "add_external_repo: pubkeyurl isn't a valid link"

  # make apt keyring directory if it doesn't exist
  if [ ! -d /usr/share/keyrings ]; then
    sudo mkdir -p /usr/share/keyrings || error "add_external_repo: failed to create apt keyring directory."
  fi

  # check if .list file already exists
  if [ -f /etc/apt/sources.list.d/${reponame}.list ]; then
    sudo rm -f /etc/apt/sources.list.d/${reponame}.list || error "add_external_repo: failed to remove conflicting .list file."
  fi

  # check if .sources file already exists
  if [ -f /etc/apt/sources.list.d/${reponame}.sources ]; then
    sudo rm -f /etc/apt/sources.list.d/${reponame}.sources || error "add_external_repo: failed to remove conflicting .sources file."
  fi

  # download gpg key from specified url
  if [ -f /usr/share/keyrings/${reponame}-archive-keyring.gpg ]; then
    sudo rm -f /usr/share/keyrings/${reponame}-archive-keyring.gpg
  fi 
  wget -qO- "$pubkeyurl" | sudo gpg --dearmor -o /usr/share/keyrings/${reponame}-archive-keyring.gpg

  if [ $? != 0 ];then
    sudo rm -f /etc/apt/sources.list.d/${reponame}.sources
    sudo rm -f /usr/share/keyrings/${reponame}-archive-keyring.gpg
    error "add_external_repo: download from specified pubkeyurl failed."
  fi

  # create .sources file
  echo "Types: deb
URIs: $uris
Suites: $suites" | sudo tee /etc/apt/sources.list.d/${reponame}.sources >/dev/null
  if [ ! -z "$components" ]; then
    echo "Components: $components" | sudo tee -a /etc/apt/sources.list.d/${reponame}.sources >/dev/null
  fi
  for input in "${@: 6}"; do
    echo "$input" | sudo tee -a /etc/apt/sources.list.d/${reponame}.sources >/dev/null
  done
  echo "Signed-By: /usr/share/keyrings/${reponame}-archive-keyring.gpg" | sudo tee -a /etc/apt/sources.list.d/${reponame}.sources >/dev/null

  if [ $? != 0 ];then
    sudo rm -f /etc/apt/sources.list.d/${reponame}.sources
    sudo rm -f /usr/share/keyrings/${reponame}-archive-keyring.gpg
    error "add_external_repo: failed to create ${reponame}.list file"
  fi
}

rm_external_repo() { # remove an external apt repo and its gpg key, if the repo is no longer in use. (force-remove the repo with force argument)

  local reponame="$1"
  local force="$2"

  [ -z "$reponame" ] && error "rm_external_repo: reponame not provided"

  # exit if reponame contains space, since apt doesn't accept .list files with spaces in filename or keyname.
  if [[ $reponame = *" "* ]]; then
    error "rm_external_repo: provided reponame contains a space."
  fi

  # always remove deprecated .list file if present
  if [ -f /etc/apt/sources.list.d/${reponame}.list ]; then
    sudo rm -f /etc/apt/sources.list.d/${reponame}.list
  fi

  # exit gracefully if .sources file does not exist
  [ -f "/etc/apt/sources.list.d/$reponame.sources" ] || return 0
  
  if [ "$force" == force ]; then
    sudo rm -f /usr/share/keyrings/${reponame}-archive-keyring.gpg || error "rm_external_repo: removal of ${reponame}-archive-keyring.gpg failed"
    sudo rm -f /etc/apt/sources.list.d/${reponame}.sources || error "rm_external_repo: removal of ${reponame}.sources failed"
    return 0
  fi

  remove_repofile_if_unused /etc/apt/sources.list.d/${reponame}.sources "" /usr/share/keyrings/${reponame}-archive-keyring.gpg
}

adoptium_installer() {
  case "$__os_codename" in
  bionic | focal | jammy | noble | buster | bullseye | bookworm)
    add_external_repo "adoptium" "https://adoptium.jfrog.io/artifactory/api/security/keypair/default-gpg-key/public" "https://adoptium.jfrog.io/artifactory/deb" "$__os_codename" "main" || exit 1
    ;;
  *)
    # use bionic target name explicitly for any other OS as its the oldest LTS target adoptium continues to support
    # all supported adoptium OSs use the same debs so the target specified does not actually matter
    add_external_repo "adoptium" "https://adoptium.jfrog.io/artifactory/api/security/keypair/default-gpg-key/public" "https://adoptium.jfrog.io/artifactory/deb" "bionic" "main" || exit 1
    ;;
  esac
  apt_update
  if [ $? != 0 ]; then
    rm_external_repo "adoptium"
    error "Failed to perform apt update after adding Adoptium repository."
  fi
}

pipx_install() {
  # install pipx keeping in mind distro issues
  # pipx < 0.16.0 is compatible with 3.6 <= python3 < 3.9
  # 0.16.0 <= pipx < 1.1.0 is compatible with python3 >= 3.6
  # 1.1.0 <= pipx < 1.3.0 is compatible with python3 >= 3.7
  # pipx >= 1.3.0 is compatible with python3 >= 3.8
  # some distros lack pipx entirely
  # some distros (raspbian bullseye specifically) have incompatible combinations of pipx (0.12.3) and python3 (3.9) versions, necessitating pipx to be installed/upgraded from pip
  # pipx 1.0.0 is the first stable release and has some features that we would like to assume are available, install it from pip if the distro package is too old
  # pi-apps scripts and functions should assume that ONLY a minimum pipx version of 1.0.0 is available
  if package_available pipx && package_is_new_enough pipx 1.0.0 ;then
    install_packages pipx python3-venv || exit 1
  elif package_is_new_enough python3 3.7 ; then
    install_packages python3-venv || exit 1
    sudo -H python3 -m pip install --upgrade pipx || exit 1
  elif package_available python3.8 ;then
    install_packages python3.8 python3.8-venv || exit 1
    sudo -H python3.8 -m pip install --upgrade pipx || exit 1
  else
    error "pipx is not available on your distro and so cannot install $* to python venv"
  fi
  sudo PIPX_HOME=/usr/local/pipx PIPX_BIN_DIR=/usr/local/bin pipx install "$@" || error "Failed to install $* with pipx"
}

pipx_uninstall() {
  sudo PIPX_HOME=/usr/local/pipx PIPX_BIN_DIR=/usr/local/bin pipx uninstall "$@" || error "Failed to uninstall $* with pipx"
}

remove_deprecated_app() { # prompts a user to uninstall a deprecated pi-apps application and then removes the application folder if it exists
  local app="$1" # on app name
  local removal_arch="$2" # 32 or 64 to only remove one architecture if specified
  local message="$3" # custom message addendum to show to the user (if given)
  [ -z "$app" ] && error "remove_deprecated_app(): requires an pi-apps app name"
  local app_status="$(app_status "${app}")"
  if [ -z "$message" ] && [ ! -z "$removal_arch" ] && [ "$arch" == "$removal_arch" ] && [ -d "${DIRECTORY}/apps/$app" ] && [ "$app_status" == "installed" ]; then
    local text="Pi-Apps has deprecated $app for ${removal_arch}-bit OSs which you currently have installed.
Would you like to uninstall it now or leave it installed? You will NOT be able to uninstall $app with Pi-Apps later."
    userinput_func "$text" "Uninstall now" "Leave installed"
  elif [ ! -z "$message" ] && [ ! -z "$removal_arch" ] && [ "$arch" == "$removal_arch" ] && [ -d "${DIRECTORY}/apps/$app" ] && [ "$app_status" == "installed" ]; then
    local text="Pi-Apps has deprecated $app for ${removal_arch}-bit OSs which you currently have installed.

$message

Would you like to uninstall it now or leave it installed? You will NOT be able to uninstall $app with Pi-Apps later."
    userinput_func "$text" "Uninstall now" "Leave installed"
  elif [ ! -z "$removal_arch" ]; then
    # remove per-architecture script regardless of the current arch
    rm -f "${DIRECTORY}/apps/$app/install-$removal_arch"
    # remove unified-architecture script incase the new version has a per-architecture script
    rm -f "${DIRECTORY}/apps/$app/install"
    return 0
  elif [ -z "$message" ] && [ -z "$removal_arch" ] && [ -d "${DIRECTORY}/apps/$app" ] && [ "$app_status" == "installed" ]; then
    local text="Pi-Apps has deprecated $app which you currently have installed.
Would you like to uninstall it now or leave it installed? You will NOT be able to uninstall $app with Pi-Apps later."
    userinput_func "$text" "Uninstall now" "Leave installed"
  elif [ ! -z "$message" ] && [ -z "$removal_arch" ] && [ -d "${DIRECTORY}/apps/$app" ] && [ "$app_status" == "installed" ]; then
    local text="Pi-Apps has deprecated $app which you currently have installed.

$message

Would you like to uninstall it now or leave it installed? You will NOT be able to uninstall $app with Pi-Apps later."
    userinput_func "$text" "Uninstall now" "Leave installed"
  elif [ -z "$removal_arch" ]; then
    # only remove folder if the desired removal arch is unset (so remove on all architectures)
    rm -rf "${DIRECTORY}/apps/$app"
    return 0
  else
    # do nothing otherwise
    return 0
  fi
  if [ "$output" == "Uninstall now" ]; then
    "${DIRECTORY}/manage" uninstall "$app"
  fi
  if [ ! -z "$removal_arch" ]; then
    # remove per-architecture script regardless of the current arch
    rm -f "${DIRECTORY}/apps/$app/install-$removal_arch"
    # remove unified-architecture script incase the new version has a per-architecture script
    rm -f "${DIRECTORY}/apps/$app/install"
  else
    # only remove folder if the desired removal arch is unset (so remove on all architectures)
    rm -rf "${DIRECTORY}/apps/$app"
  fi
  return 0
}

terminal_manage() { # wrapper for the original terminal_manage function to terminal_mange_multi
  action="$1"
  app="$2" #one app name
  
  [ -z "$action" ] && error "terminal_manage(): Must specify an action: either 'install' or 'uninstall' or 'update' or 'refresh'"
  
  terminal_manage_multi "$action $app"
}

terminal_manage_multi() { #function to install/uninstall/update/refresh apps or refresh-file files, one per line - refreshes the gui app list if applicable
  local queue="$1" #one action per line, in the format "$action $app" or "update-file $file"
  
  #To prevent multiple simultaneous manage instances, use the 'daemon' mode. This will create a queue of actions that are executed concurrently.
  #The first daemon instance is the 'master' process, which opens a terminal. Subsequent processes will add the action to the queue and then exit.
  #All output is generated by the 'master' daemon process. Subsequent processes shouldn't open a terminal, because the master one is already open.
  if [ -f "${DIRECTORY}/data/manage-daemon/pid" ] && process_exists $(cat "${DIRECTORY}/data/manage-daemon/pid") ;then
    #daemon already running, send queue to it and exit
    "${DIRECTORY}/manage" daemon "$queue"
    
  else
    #this function is running the daemon, so once it exits refresh the pi-apps list
    "${DIRECTORY}/manage" daemon "$queue"
    
    #refresh app list
    if [ ! -z "$pipe" ] && [ -p "$pipe" ];then
      echo -e '\f' > "$pipe"
      "${DIRECTORY}/preload" "$(cat "${DIRECTORY}/data/settings/App List Style")" "$prefix" > "$pipe" 2>/dev/null
    fi
  fi
}
#end of apt functions

#flatpak functions
flatpak_install() { #install an app using flatpak
  [ -z "$1" ] && error "flatpak_install(): This function is used to install a flatpak app, but nothing was specified."
  
  #make sure flatpak is installed
  if ! command -v flatpak >/dev/null ;then
    error "flatpak_install(): Could not install $1 because flatpak is not installed!"
  fi
  
  if ! package_is_new_enough flatpak 1.14.4 ;then
    case "$__os_codename" in
    buster)
      debian_ppa_installer "theofficialgman/flatpak-no-bwrap" "bionic" "0ACACB5D1E74E484"
      apt_lock_wait
      sudo apt --only-upgrade install flatpak -y | less_apt
      ;;
    bullseye)
      debian_ppa_installer "theofficialgman/flatpak-no-bwrap" "focal" "0ACACB5D1E74E484"
      apt_lock_wait
      sudo apt --only-upgrade install flatpak -y | less_apt
      ;;
    bionic|focal|jammy)
      ubuntu_ppa_installer "theofficialgman/flatpak-no-bwrap"
      apt_lock_wait
      sudo apt --only-upgrade install flatpak -y | less_apt
      ;;
    esac
  fi
  
  status -n "Flatpak: Adding flathub remote... "
  #Add the flathub remote, first as root, if that fails then try installing as user, while removing unwanted output
  ( sudo flatpak remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo || flatpak remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo || error "Flatpak failed to add flathub remote!"  ) | grep --line-buffered -v "Note that the directories 
'/var/lib/flatpak/exports/share'
'$HOME/.local/share/flatpak/exports/share'
are not in the search path set by the XDG_DATA_DIRS environment variable, so
applications installed by Flatpak may not appear on your desktop until the
session is restarted."
  #exit 1 if above code failed
  if [ ${PIPESTATUS[0]} != 0 ];then
    exit 1
  fi
  status_green Done
  
  status -n "Flatpak: installing $1... "
  #Install the specified app, first as root, if that fails then try installing as user, while removing unwanted output
  ( sudo flatpak install flathub "$1" -y || flatpak install flathub "$1" -y || error "Flatpak failed to install $1!" ) | grep --line-buffered -v "Note that the directories 

'/var/lib/flatpak/exports/share'
'$HOME/.local/share/flatpak/exports/share'
are not in the search path set by the XDG_DATA_DIRS environment variable, so
applications installed by Flatpak may not appear on your desktop until the
session is restarted."
  #exit 1 if above code failed
  if [ ${PIPESTATUS[0]} != 0 ];then
    exit 1
  fi
  status_green Done
  
  #Pi-Apps tries to avoid unnecessary reboots at all cost. Flatpak places desktop launchers in /var/lib/flatpak/exports/share/applications, which is not searched by default.
  #This path is added to $XDG_DATA_DIRS on the next reboot, but we don't want to wait for that!
  #If there are files in /var/lib/flatpak/exports/share/applications, and XDG_DATA_DIRS is missing flatpak paths, then bind-mount to /usr/share/applications
  if [[ "$XDG_DATA_DIRS" != */var/lib/flatpak/exports/share* ]] && [ ! -z "$(ls /var/lib/flatpak/exports/share/applications)" ] && [ -z "$(ls /usr/share/applications/flatpak-temporary)" ];then
    sudo mkdir -p /usr/share/applications/flatpak-temporary
    sudo mount --bind /var/lib/flatpak/exports/share/applications /usr/share/applications/flatpak-temporary
  elif [[ "$XDG_DATA_DIRS" == */var/lib/flatpak/exports/share* ]] ;then
    sudo rm -rf /usr/share/applications/flatpak-temporary
  fi
  #Additionally, PiOS Buster had a bug where XDG_DATA_DIRS was missing flatpak's entries due to PiOS mods. Pi-Apps fixes this with a runonce.
  
  true
}

flatpak_uninstall() { #uninstall an app using flatpak
  [ -z "$1" ] && error "flatpak_uninstall(): This function is used to uninstall a flatpak app, but nothing was specified."
  
  #if flatpak is not installed, then skip everything with code 0.
  if ! command -v flatpak >/dev/null ;then
    return 0
  fi
  
  #Only try to remove flatpak app if it's installed.
  if flatpak list | grep -qF "$1" ;then
    sudo flatpak uninstall "$1" -y || flatpak uninstall "$1" -y || error "Flatpak failed to uninstall $1!"
  fi
}
#end of flatpak functions

#app functions
list_apps() { # $1 can be: installed, uninstalled, corrupted, cpu_installable, hidden, visible, online, online_only, local, local_only
  if [ -z "$1" ] || [ "$1" == local ];then
    #list all apps
    ls "${DIRECTORY}/apps"
    
  elif [ "$1" == all ];then
    #combined list of apps, both online and local. Removes duplicate apps from the list.
    echo -e "$(list_apps local)\n$(list_apps online)" | sort | uniq
    
  elif [ "$1" == installed ];then
    #list apps      |   only show      (          list of installed apps                | remove match string  |   basename   )
    list_apps local | list_intersect "$(grep -rx 'installed' "${DIRECTORY}/data/status" | awk -F: '{print $1}' | sed 's!.*/!!')"
    
  elif [ "$1" == corrupted ];then
    #list apps      |only show         (          list of corrupted apps                | remove match string  |   basename   )
    list_apps local | list_intersect "$(grep -rx 'corrupted' "${DIRECTORY}/data/status" | awk -F: '{print $1}' | sed 's!.*/!!')"
    
  elif [ "$1" == disabled ];then
    #list apps      |    only show     (          list of disabled apps                | remove match string  |   basename   )
    list_apps local | list_intersect "$(grep -rx 'disabled' "${DIRECTORY}/data/status" | awk -F: '{print $1}' | sed 's!.*/!!')"
    
  elif [ "$1" == uninstalled ];then
    #list apps that have a status file matching "uninstalled"
    list_apps local | list_intersect "$(grep -rx 'uninstalled' "${DIRECTORY}/data/status" | awk -F: '{print $1}' | sed 's!.*/!!')"
    #also list apps that don't have a status file
    list_apps local | list_subtract "$(ls "${DIRECTORY}/data/status")"
    
  elif [ "$1" == have_status ];then
    #list apps that have a status file
    list_apps local | list_intersect "$(ls "${DIRECTORY}/data/status")"
    
  elif [ "$1" == missing_status ];then
    #list apps that don't have a status file
    list_apps local | list_subtract "$(ls "${DIRECTORY}/data/status")"
    
  elif [ "$1" == cpu_installable ];then
    #list apps that can be installed on the device's OS architecture (32-bit or 64-bit)
    #find all apps that have install-XX script, install script, or a packages file
    find "${DIRECTORY}/apps" -type f \( -name "install-$arch" -o -name "install" -o -name "packages" \) | sed "s+${DIRECTORY}/apps/++g" | sed 's+/.*++g' | sort | uniq
    
  elif [ "$1" == package ];then
    #list apps that have a "packages" file
    find "${DIRECTORY}/apps" -type f -name "packages" | sed "s+/packages++g" | sed "s+${DIRECTORY}/apps/++g" | sort | uniq
    
  elif [ "$1" == standard ];then
    #list apps that have scripts
    find "${DIRECTORY}/apps" -type f \( -name "install-32" -o -name "install-64" -o -name "install" -o -name "uninstall" \) | sed "s+${DIRECTORY}/apps/++g" | sed 's+/.*++g' | sort | uniq
    
  elif [ "$1" == hidden ];then
    #list apps that are hidden
    read_category_files | grep '|hidden' | awk -F'|' '{print $1}'
    
  elif [ "$1" == visible ];then
    #list apps that are in any other category but 'hidden', and aren't disabled
    read_category_files | grep -v '|hidden' | awk -F'|' '{print $1}' # | list_subtract "$(list_apps disabled)"
    
  elif [ "$1" == online ];then
    #list apps that exist on the online git repo
    if [ -d "${DIRECTORY}/update/pi-apps/apps" ];then
      #if update folder exists, just use that
      ls "${DIRECTORY}/update/pi-apps/apps" | grep .
    else
      #if update folder doesn't exist, then parse github HTML to get a list of online apps. Horrible idea, but it works!
      wget -qO- "${repo_url}/tree/master/apps" | grep 'title=".*" data-pjax=' -o | sed 's/title="//g' | sed 's/" data-pjax=//g'
    fi
    
  elif [ "$1" == online_only ];then
    #list apps that exist only on the git repo, and not locally
    list_apps online | list_subtract "$(list_apps local)"
    
  elif [ "$1" == local_only ];then
    #list apps that exist only locally, and not on the git repo
    list_apps local | list_subtract "$(list_apps online)"
    
  else
    error "list_apps(): unrecognized filter '$1'!"
  fi
}

list_intersect() { #Outputs only the apps that appear in both stdin and in $1
  # for example, the following two inputs will be a match
  # Audacity
  # Audacity
  # while these two will NOT be a match
  # Multimedia/Audacity
  # .*/Audacity
  comm -12 - <(echo "$1" | sort)
}

list_intersect_partial() { #Outputs only the apps that appear in both stdin and in $1 even with a partial match
  # for example, the following two inputs will be a match
  # Multimedia/Audacity
  # .*/Audacity
  #                      change \n to \|     |   remove last "\|"
  grep -x "$(echo "$1" | sed -z 's/\n/\\|/g' | sed -z 's/\\|$/\n/g')"
}

list_subtract() { #Outputs a list of apps from stdin, minus the ones that appear in $1
  # for example, the following two inputs will be a match
  # Audacity
  # Audacity
  # while these two will NOT be a match
  # Multimedia/Audacity
  # .*/Audacity
  comm -23 - <(echo "$1" | sort)
}

list_subtract_partial() { #Outputs a list of apps from stdin, minus the ones that appear in $1 even with a partial match
  # for example, the following two inputs will be a match
  # Multimedia/Audacity
  # .*/Audacity
  #                       change \n to \|     |   remove last "\|"
  grep -vx "$(echo "$1" | sed -z 's/\n/\\|/g' | sed -z 's/\\|$/\n/g')"
}

read_category_files() { #Generates a combined categories-list from several sources: category-overrides, global categories file, and unlisted apps. Format: "app|category"
  
  #remove app category if app folder not found
  local IFS=$'\n'
  local app
  for app in $(cat "${DIRECTORY}/data/category-overrides" 2>/dev/null); do
    if ! [ -d "${DIRECTORY}/apps/$(echo "$app" | sed 's/|.*//')" ] &>/dev/null; then 
      sed -i "/$app/d"  "${DIRECTORY}/data/category-overrides"
    fi
  done

  # get device specific categories overrides (if they exist)
  # obtain model and jetson_model
  get_model &>/dev/null
  version_id=$(grep 'VERSION_ID=' /etc/os-release | tr -cd '0123456789.')
  unset device_override
  if [[ "$model" != *"Raspberry Pi"* ]]; then
    device_override="${DIRECTORY}/etc/category-overrides-non-raspberry"
  fi
  if [[ ! -z "$jetson_model" ]] && [[ "$version_id" == "18.04" ]]; then
    device_override="${DIRECTORY}/etc/category-overrides-jetson-18.04"
  elif [[ ! -z "$jetson_model" ]]; then
    device_override="${DIRECTORY}/etc/category-overrides-jetson-generic"
  fi
  
  #list the user-overrides file     and the device specific override  and the global categories file   |-----and all apps------------|     filter out duplicates   no '\n\n'
  (cat "${DIRECTORY}/data/category-overrides" "$device_override" "${DIRECTORY}/etc/categories" 2>/dev/null ; echo ; list_apps local | sed 's/$/|/g') | awk -F'|' '!seen[$1]++' | grep .
}

app_prefix_category() { #lists all apps in a category with format "category/app", or if $1 is left blank, then list the full structure of all categories
  
  #Subtract a type of app if enabled
  local show_apps_setting="$(cat "${DIRECTORY}/data/settings/Show apps")"
  local filter=()
  if [ "$show_apps_setting" == 'standard' ];then
    #if only showing standard apps, hide package apps
    filter=(list_subtract_partial "$(list_apps package | sed 's+^+.*/+g')")
  elif [ "$show_apps_setting" == 'packages' ];then
    #if only showing package apps, hide standard apps
    filter=(list_subtract_partial "$(list_apps standard | sed 's+^+.*/+g')")
  else
    #is the setting is "all" or missing, don't filter.
    filter=(cat)
  fi
  
  #show special "Installed" category - don't filter it
  if [ "$1" == "Installed" ]; then
    list_apps installed | sed 's+^+Installed/+g'
  #show special "Packages" category
  elif [ "$1" == "Packages" ]; then
    list_apps package | list_subtract "$(list_apps hidden)" | sed 's+^+Packages/+g'
  #show special "All Apps" category
  elif [ "$1" == "All Apps" ]; then
    list_apps cpu_installable | list_subtract "$(list_apps hidden)" | sed 's+^+All Apps/+g' | "${filter[@]}"
  # show all categories
  elif [ -z "$1" ]; then
    #show normal categories
    read_category_files | grep . | awk -F'|' '{print $2"/"$1}' | sed 's+^/++g' | "${filter[@]}"
    
    #show special "Installed" category - don't filter it
    list_apps installed | sed 's+^+Installed/+g'
    
    #show special "Packages" category
    if [ "$show_apps_setting" != standard ];then
      list_apps package | list_subtract "$(list_apps hidden)" | sed 's+^+Packages/+g'
    fi
    #show special "All Apps" category
    list_apps cpu_installable | list_subtract "$(list_apps hidden)" | sed 's+^+All Apps/+g' | "${filter[@]}"
  #show normal categories
  else
    read_category_files | grep . | awk -F'|' '{print $2"/"$1}' | grep "^$1/" | sed 's+^/++g' | "${filter[@]}"
  fi
}

bitly_link() { #compatibility function pointing to shlink_link (incase old manage script is running with new api)
  shlink_link "$@"
}

shlink_link() { #Runs whenever an app is installed/uninstalled to tally the number of users for each app
  {
  #This cannot possibly be used to identify you, or any information about you.
  #It simply "clicks" a shlink link - a shortened URL - so that the total number of clicks can be tallied to determine how popular a certain app is.
  local app="$1"
  local trigger="$2"
  
  [ -z "$app" ] && error "shlink_link(): requires an app argument"
  [ -z "$trigger" ] && error "shlink_link(): requires a trigger argument"
  
  #if the 'Enable Analytics' setting is enabled
  if [ "$(cat "${DIRECTORY}/data/settings/Enable analytics" 2>/dev/null)" != 'No' ]; then
    #obtain model and SOC_ID
    get_model &>/dev/null
    local model="$(echo $model | tr -d '"'\; | head -1)"
    # obtain (hashed) machine_id (only if file exists and has contents)
    [ -s /etc/machine-id ] && machine_id="$(cat /etc/machine-id | sha1sum | awk '{print $1}' | head -1)"
    # obtain (hashed) serial_number (only if file exists and has contents)
    [ -s /sys/firmware/devicetree/base/serial-number ] && serial_number="$(cat /sys/firmware/devicetree/base/serial-number | sha1sum | awk '{print $1}' | head -1)"
    #obtain operating system name
    #not using PRETTY_NAME to only get the ID and OS major version (eg: Ubuntu 24.04 instead of Ubuntu 24.04.1 LTS)
    local os_name="$(cat /etc/os-release | grep ^ID= | tr -d '"'\; | awk -F= '{print $2}' | head -1) $(cat /etc/os-release | grep ^VERSION_ID= | tr -d '"'\; | awk -F= '{print $2}' | head -1)"

    #determine the name of the link to "click"
    local shlinklink="https://analytics.pi-apps.io/pi-apps-$trigger-$(echo "$app" | tr -d ' ' | sed 's/[^a-zA-Z0-9]//g')/track"
    #click it
    #NOTE: any future changes to the user agent should only APPEND
    curl -s -X 'GET' "$shlinklink" -H 'accept: image/gif' -A "Pi-Apps Raspberry Pi app store; $model; $SOC_ID; $machine_id; $serial_number; ${os_name^}; $arch" >/dev/null
  fi
  } &
}

usercount() { #Return number of users for specified app. $1 is app name. If empty, all are shown.
  #If clicklist file missing or over a day old, download it.
  if [ ! -f "${DIRECTORY}/data/clicklist" ] || [ ! -z "$(find "${DIRECTORY}/data/clicklist" -mtime +1 -print)" ]; then
    wget 'https://raw.githubusercontent.com/Botspot/pi-apps-analytics/main/clicklist' -qO "${DIRECTORY}/data/clicklist" >/dev/null || return 1
  fi
  clicklist="$(cat "${DIRECTORY}/data/clicklist")"
  
  [ -z "$clicklist" ] && error "usercount(): clicklist empty. Likely no internet connection"
  
  if [ -z "$1" ];then
    echo "$clicklist"
  else
    # $1 is app
    echo "$clicklist" | grep "[0-9] $1"'$' | awk '{print $1}' | head -n1
  fi
  
}

script_name() { #returns name of install script(s) for the $1 app. outputs: '', 'install-32', 'install-64', 'install', 'install-32 install-64'
  [ -z "$1" ] && error 'script_name(): requires an argument'
  
  #ensure $1 is valid app name
  [ ! -d "${DIRECTORY}/apps/$1" ] && error "script_name: '$1' is an invalid app name.\n${DIRECTORY}/apps/$1 does not exist."
  
  if [ -f "${DIRECTORY}/apps/$1/install-32" ] && [ ! -f "${DIRECTORY}/apps/$1/install-64" ];then
    echo 'install-32'
  elif [ -f "${DIRECTORY}/apps/$1/install-64" ] && [ ! -f "${DIRECTORY}/apps/$1/install-32" ];then
    echo 'install-64'
  elif [ -f "${DIRECTORY}/apps/$1/install-64" ] && [ -f "${DIRECTORY}/apps/$1/install-32" ];then
    echo 'install-32 install-64'
  elif [ -f "${DIRECTORY}/apps/$1/install" ];then
    echo 'install'
  else
    true
    #error "No install script found for the $app app! Please report this to Botspot."
  fi
  
}

script_name_cpu() { #get script name to run based on detected CPU arch
  [ -z "$1" ] && error 'script_name_cpu(): requires an argument.'
  
  #ensure $1 is valid app name
  if ! list_apps all | grep -q "$1" ;then
    error "script_name_cpu: '$1' is an invalid app name."
  fi
  
  if [ -f "${DIRECTORY}/apps/$1/install-32" ] && [ $arch == 32 ];then
    echo 'install-32'
  elif [ -f "${DIRECTORY}/apps/$1/install-64" ] && [ $arch == 64 ];then
    echo 'install-64'
  elif [ -f "${DIRECTORY}/apps/$1/install" ];then
    echo 'install'
  elif [ -f "${DIRECTORY}/apps/$1/packages" ];then
    echo 'packages'
  else
    true #app not compatible with current arch
  fi
}

app_status() { #Gets the $1 app's current status. installed, uninstalled, corrupted, disabled
  
  [ -z "$1" ] && error 'app_status(): requires an argument.'
  #don't check if app exists, it may be a new app in an update
  
  if [ -f "${DIRECTORY}/data/status/${1}" ];then
    cat "${DIRECTORY}/data/status/${1}"
  else
    echo 'uninstalled' #if app status file doesn't exist, assume uninstalled
  fi
}

app_type() { #there are 'standard' apps, and there are 'package' apps - an alias to install an apt package from the existing repositories.
  #$1 is input app-name
  local app="$1"
  [ -z "$app" ] && error "app_type(): no app specified!"
  
  if [ -f "${DIRECTORY}/apps/${app}/packages" ];then
    echo package
  elif [ -f "${DIRECTORY}/apps/${app}/uninstall" ] || [ -f "${DIRECTORY}/apps/${app}/install" ] || [ -f "${DIRECTORY}/apps/${app}/install-32" ] || [ -f "${DIRECTORY}/apps/${app}/install-64" ];then
    echo standard
  else
    return 1
  fi
  #if neither conditional above evaluated to true, no output will be returned and the function exits with code 1
}

pkgapp_packages_required() { #Returns which packages are required during installation of a package-app - handle the '|' separater
  #returns no output if not all required packages are available
  local app="$1"
  [ -z "$app" ] && error "pkgapp_packages_required(): no app specified!"
  
  if [ ! -f "${DIRECTORY}/apps/$app/packages" ];then
    error "pkgapp_packages_required(): This app '$app' does not have a packages file!"
  fi
  
  local IFS=' '
  local word=''
  local packages=''
  #read each word - packages separated by '|' are 1 word
  for word in $(cat "${DIRECTORY}/apps/$app/packages" | sed 's/ | /|/g') ;do
    
    if [[ "$word" == *'|'* ]];then
      IFS='|'
      local package
      local found="no"
      #first check for any already installed packages
      #if a package is already installed, it should be used even if it is not the first option in the OR
      for package in $word ;do
        if package_installed "$package" ;then
          packages+="$package "
          found="yes"
          break
        fi
      done
      if [ "$found" == "yes" ]; then
        #a package in the OR is already installed
        continue
      fi
      #then check for available packages
      for package in $word ;do
        if package_available "$package" ;then
          packages+="$package "
          found="yes"
          break
        fi
      done
      if [ "$found" == "no" ]; then
        #no package in the OR is available so set the output as empty
        packages=''
        break
      fi
    else #non-OR package - no parsing '|' separators
      if package_available "$word" ;then
        #no separator, so return it without change
        packages+="$word "
      else
        #one package in the AND is not available so set the output as empty
        packages=''
        break
      fi
    fi
  done
  [ ! -z "$packages" ] && echo "${packages::-1}" #remove final space character
}

will_reinstall() { #return 0 if $1 app will be reinstalled during an update, otherwise return 1.
  local app="$1"
  [ -z "$app" ] && error 'will_reinstall(): requires an argument'
  
  #exit immediately if app is not installed.
  if [ "$(app_status "$app")" != 'installed' ];then
    return 1
  fi
  #it seems that the above code was added for speed alone https://github.com/Botspot/pi-apps/commit/d8bfc3f5fc9b42060bfbd68a11b24f66246d61bc
  #commenting it out allows retrying failed app update where uninstall succeeds but install fails
  #edit: uncommented it again because all uninstalled app refreshes were now being installed
  
  #detect which installation script exists - both for local install and for update directory
  local local_scriptname="$(script_name_cpu "$app")"
  
  local new_scriptname="$(DIRECTORY="${DIRECTORY}/update/pi-apps" script_name_cpu "$app")"
  
  if [ ! -z "$new_scriptname" ] && [ "$local_scriptname" == packages ] && [ "$new_scriptname" != packages ];then
    #migration from package-app to script-app. Reinstall.
    return 0
  elif [ "$local_scriptname" != packages ] && [ "$new_scriptname" == packages ];then
    #migration from script-app to package-app. Reinstall.
    return 0
  elif [ "$new_scriptname" == packages ];then
    #update to package-app: rather than check for file change, check if the installed package(s) would be different (avoid reinstall when adding an alternative package name with '|')
    if [ "$(pkgapp_packages_required "$app")" != "$(DIRECTORY="${DIRECTORY}/update/pi-apps" pkgapp_packages_required "$app")" ];then
      return 0
    else
      return 1
    fi
  elif [ ! -z "$new_scriptname" ] && ! files_match "${DIRECTORY}/update/pi-apps/apps/${app}/${new_scriptname}" "${DIRECTORY}/apps/${app}/${local_scriptname}" ;then
    #if install script exists in update folder, and if install script was changed, and if installed already
    return 0
  else
    return 1
  fi
}

app_search() { #search all apps for $1, with filenames in $2 (space-separated and optional)
  local query="$1"
  [ -z "$query" ] && error "app_search(): requires a search query."
  
  #you can change which app-files are searched. Default: description, website, credits
  if [ ! -z "${2+x}" ];then
    #second argument can be set, but left blank
    local search_files="$(echo "$2" | sed 's/^ //g' | sed 's/ $//g' | sed 's/  / /g')"
  else
    #if no second argument provided
    local search_files="description website credits"
  fi
  
  #generate syntax for the find command. Example output: "-name description -o -name website -o -name credits"
  local find_syntax="$(echo "$search_files" | sed 's/ / -o -name /g')"
  echo "search pattern: $find_syntax" 1>&2
  
  #search app files
  if [ -z "$search_files" ];then
    local results=""
  else
    local results="$(find "${DIRECTORY}/apps" \( -name $find_syntax \) -exec grep -m1 -Fi "$query" {} + | awk -F: '{print $1}' | sed "s+${DIRECTORY}/apps/++g" | sed 's+/.*++g' | sort | uniq)"
  fi
  #hide incompatible and hidden/disabled apps
  results="$(echo "$results" | list_subtract "$(list_apps hidden)" | list_intersect "$(list_apps cpu_installable)")"
  
  if [ -z "$app_list" ];then
    local app_list="$(list_apps cpu_installable | list_subtract "$(list_apps hidden)")"
  fi
  
  #search app names - first prioritize results starting with query, then show results containing query, then show other pre-existing results.
  results="$(grep -i "^$query" <<<"$app_list" | sort)
$(grep -i "$query" <<<"$app_list" | sort)
$results"
  
  #remove duplicate entries
  echo "$results" | awk '!seen[$0]++'
  
}

app_search_gui() { #graphical interface to search for apps
  
  #pre-populate search field with last search, if available
  if [ -f "${DIRECTORY}/data/last-search" ];then
    local last_search="$(cat "${DIRECTORY}/data/last-search")"
  else
    local last_search=''
  fi
  
  local output=''
  output="$(yad "${yadflags[@]}" --title=Search --width=310 \
    --text="Search for apps."$'\n'"Not case-sensitive." \
    --form --field='' "$last_search" \
    --field='Search description':CHK 'TRUE' \
    --field='Search website':CHK 'TRUE' \
    --field='Search credits':CHK 'FALSE' \
    --field='Search scripts':CHK 'FALSE')" || return 0
  local query="$(echo "$output" | sed -n 1p)"
  
  #exit now if search query is empty
  if [ -z "$query" ];then
    return 0
  fi
  #save query for next time in last-search data file
  echo "$query" > "${DIRECTORY}/data/last-search"
  
  #skip file-based search if user searched for exact app name
  local app_list="$(list_apps cpu_installable | list_subtract "$(list_apps hidden)")"
  local results="$(grep -xFi "$query" <<<"$app_list")"
  if [ ! -z "$results" ];then
    echo "$results"
    return 0
  fi
  
  local search_files='description website credits scripts'
  search_files=''
  echo "$output" | sed -n 2p | grep -q 'TRUE' && search_files+='description '
  echo "$output" | sed -n 3p | grep -q 'TRUE' && search_files+='website '
  echo "$output" | sed -n 4p | grep -q 'TRUE' && search_files+='credits '
  echo "$output" | sed -n 5p | grep -q 'TRUE' && search_files+='install install-32 install-64 uninstall '
  
  #echo -e "query: $query\nsearch_files: $search_files"
  
  results="$(app_search "$query" "$search_files")"
  
  if [ ! -z "$results" ] && [ "$(echo "$results" | wc -l)" == 1 ];then
    #if only one result, don't ask user to select it
    echo "$results" #return result to outside world
    
  elif [ ! -z "$results" ];then #display apps and their categories in a list for user to select
    
    local app="$(show_app() {
      echo "$2"
    }
    export -f show_app
    
    categories="$(read_category_files)"
    
    IFS=$'\n'
    for app in $results ;do
      local category="$(grep '^'"$app"'|' <<<"$categories" | awk -F'|' '{print $2}')"
      
      echo "${DIRECTORY}/apps/${app}/icon-24.png
$app
$( (cat "${DIRECTORY}/apps/${app}/description" || echo "Description unavailable") | head -n1)
<small>in</small>
${DIRECTORY}/icons/categories/${category##*/}.png
${category}"
    done | \
    yad "${yadflags[@]}" --title="Results for "\""$query"\""" --width=310 --height=250 \
      --list --no-headers  --column=app:IMG --column=appname --column=tooltip:HD --column=in --column=category:IMG --column=categoryname \
      --tooltip-column=3 --no-buttons \
      --select-action="bash -c "\""show_app %s; kill "\$"YAD_PID"\""")"
    
    echo "$app" #return result to outside world
    
  elif list_apps | grep -qi "^$query" ;then
    #no compatible apps found, but an incompatible app was found that matches the name
    yad "${yadflags[@]}" --title=Results --width=310 \
      --text=""\""<b>$(list_apps | grep -i -m1 "^$query")</b>"\"" is not compatible with your ${__os_desc} ${arch}-bit OS." \
      --button=OK:0
    return 0
    
  else #no results found
    yad "${yadflags[@]}" --title=Results --width=310 \
      --text="No results found for "\""<b>$query</b>"\""." \
      --button=OK:0
    return 0
  fi
}

userinput_func() { # userinput function to display yad/cli prompts to the user
  [ -z "$1" ] && error "userinput_func(): requires a description"
  [ -z "$2" ] && error "userinput_func(): requires at least one output selection option"
  local text_lines=$(echo -e "$1" | wc -l)
  # there is no good universal way to calculate the required height of the window
  # the users theme, default text size, and window scaling all affect it
  # the idea is the height should be a function of the number of lines of input text and the number of list options
  local height_list=$(echo $(( text_lines * 17 + $(( ${#@} - 1 )) * 29 + 65 )))
  if [ ! -z "$screen_height" ] && (( $height_list > "$screen_height" )); then
    height_list="$screen_height"
  elif [ -z "$screen_height" ] && (( $height_list > "720" )); then
    height_list="720"
  fi
  local commonflags=(--fixed --no-escape --undecorated --center --borders=20)
  if [ "${#@}" == "2" ];then
    yad "${yadflags[@]}" "${commonflags[@]}" \
      --image "dialog-information" \
      --text="$1" \
      --button="$2":0
    output="$2"
  elif [ "${#@}" == "3" ];then
    yad "${yadflags[@]}" "${commonflags[@]}" \
      --image "dialog-question" \
      --text="$1" \
      --button="$2":0 \
      --button="$3":1
    if [ $? -ne 0 ]; then
      output="$3"
    else
      output="$2"
    fi
  else
    unset uniq_selection
    for string in "${@:2}"; do
      local uniq_selection+=(FALSE "$string")
    done
    local uniq_selection[0]=TRUE
    output=$(yad "${yadflags[@]}" "${commonflags[@]}" \
      --height=$height_list\
      --text "$1" \
      --list \
      --no-headers \
      --radiolist \
      --center \
      --column "" \
      --column "Selection" \
      --print-column=2 \
      --separator='' \
      --button="OK":0 \
      "${uniq_selection[@]}")
  fi
}

generate_app_icons() { #This converts the given $1 image into icon-24.png and icon-64.png files for the $2 app
  icon="$1"
  app="$2"
  
  [ -z "$icon" ] && error "create_app_icons(): icon field empty!"
  [ -z "$app" ] && error "create_app_icons(): app field empty!"
  
  #ensure imagemagick is installed
  if ! command -v convert >/dev/null ;then
    yad "${yadflags[@]}" --text="To resize the images, imagemagick must be installed."$'\n'"Install now?" \
    --text-align=center --title='Quick question' \
    --button=No!"${DIRECTORY}/icons/exit.png":1 --button=Yes!"${DIRECTORY}/icons/check.png":0
    button=$?
    if [ $button == 0 ];then
      sudo apt-get install -y --no-install-recommends imagemagick || icon=''
    else
      exit 0
    fi
  fi
  
  #scale it to 24x24
  convert "$icon" -resize 24x24 "${DIRECTORY}/apps/${app}/icon-24.png"
  
  #scale it to 64x64
  convert "$icon" -resize 64x64 "${DIRECTORY}/apps/${app}/icon-64.png"

}

refresh_pkgapp_status() { #for the specified package-app, if dpkg thinks it's installed, then mark it as installed.
  local app="$1"
  [ -z "$app" ] && error "refresh_pkgapp_status(): no app specified!"

  # optional: directly pass package as second input argument (can be null for when you want to mark an app as hidden)
  if [ -z ${2+x} ]; then
    #From the list of necessary packages for the $app app, get the first one that is available in the repos
    local package="$(pkgapp_packages_required "$app" | awk '{print $1}')"
  else
    local package="$(echo "$2" | awk '{print $1}')"
  fi

  if [ -z "$package" ]; then
    #this app is trying to install a package that's not on the repository. Hide the app.
    echo "Marking $app as hidden"
    "${DIRECTORY}/etc/categoryedit" "$app" 'hidden' >/dev/null
    return
  #if that package is installed
  elif package_installed "$package" ;then
    #mark this app as installed
    if [ "$(app_status "$app")" != 'installed' ];then
      echo "Marking $app as installed"
      echo 'installed' > "${DIRECTORY}/data/status/${app}"
      shlink_link "$app" install &
    fi
  #if that package is not installed, then it only exists on the repositories, the pkgapp_packages_required function output guarantees it
  else
    #the package for the $app app is not installed but it is available, so mark this app as uninstalled
    if [ "$(app_status "$app")" != 'uninstalled' ];then
      echo "Marking $app as uninstalled"
      rm -f "${DIRECTORY}/data/status/${app}"
      shlink_link "$app" uninstall &
    fi
  fi

  #package is hidden but available. Show it.
  if grep -qxF "${app}|hidden" "${DIRECTORY}/data/category-overrides" ;then
    #move app to original category
    #this should not cause a tug-of-war if a package-app is ever hidden with one of the OS-specific overrides files, because category detection is constrained to the category-overrides file.
    echo "Unhiding $app as its packages are now available"
    "${DIRECTORY}/etc/categoryedit" "$app" "$(grep "^${app}|" "${DIRECTORY}/etc/categories" | awk -F'|' '{print $2}')" >/dev/null
  fi
}

refresh_all_pkgapp_status() { #for every package-app, if dpkg thinks it's installed, then mark it as installed.
  #repeat for every package-type app
  local IFS=$'\n'
  local dpkg_arch="$(dpkg --print-architecture)"
  # get list of all packages needed by package apps
  # this variable needs to be global to be accessible from the subshells
  local packages="$(sed '' -- "${DIRECTORY}"/apps/*/packages | sed 's/ | /\n/g' | sed 's/ /\n/g' | awk NF | sed 's/$/:'"$dpkg_arch"'/' | tr '\n' ' ')"
  packages="${packages::-1}" #remove final space character

  # get policy info for all packages needed by package apps
  # this variable needs to be global to be accessible from the subshells
  local apt_cache_output="$(echo "$packages" | xargs -r apt-cache policy)"

  #redefine package_installed to only read /var/lib/dpkg/status once
  local dpkg_status="$(grep -x "Package: \($(echo "$packages" | sed 's/:'"$dpkg_arch"'//g ; s/ /\\|/g')\)" -A 2 /var/lib/dpkg/status)"

  #redefine package_available() to use apt_cache_output and avoid running apt-cache multiple times
  (package_available() { #this will only be used in this function's subprocesses.
    echo "$apt_cache_output" | grep -x "${1}:" -A2 | grep -vxF "  Candidate: (none)" | grep -q "^  Candidate:"
  }
  
  #this one only takes off 0.1s on my pi5, so if it causes issues it could be removed
  package_installed() { #exit 0 if $1 package is installed, otherwise exit 1
    local package="$1"
    [ -z "$package" ] && error "package_installed(): no package specified!"
    #find the package listed in /var/lib/dpkg/status
    #package_info "$package"

    #directly search /var/lib/dpkg/status
    echo "$dpkg_status" | grep -x "Package: $package" -A 2 -m1 | grep -qxF 'Status: install ok installed'
  }

  # parse apt_cache_output for each package app
  # generate list of all packages needed by package apps
  for app in $(list_apps package) ;do
    refresh_pkgapp_status "$app" #with redefined package_available and package_installed this is fast
  done)
}

refresh_app_list() { #Force-regenerate the app list
  local guimode="$(cat "${DIRECTORY}/data/settings/App List Style")"
  
  #delete preload directory, then re-generate it
  rm -rf "${DIRECTORY}/data/preload"
  "${DIRECTORY}/etc/preload-daemon" "$guimode" once &>/dev/null
}

list_apps_missing_dummy_debs() { #List any installed apps that have had their dummy deb uninstalled more recently than the app was installed
  local IFS=$'\n'
  local app
  local dummy_deb_status="$(grep ' installed pi-apps-\| not-installed pi-apps-' /var/log/dpkg.log | tac | awk -F' ' '!seen[$5]++' | sed 's/:all$//g')"
  #example line value: 2023-03-20 13:13:54 status installed pi-apps-6f59c0e1:all 1.0
  
  for app in $(list_apps installed | list_intersect "$(list_apps standard)") ;do
    #For every installed app, make sure the dummy deb, if it has ever been installed, is installed
    local pkgname="$(app_to_pkgname "$app")"
    local match="$(grep -m1 "$pkgname" <<<"$dummy_deb_status")"
    
    if [ -z "$match" ];then
      #echo "no dummy deb information associated with $app"
      true
    elif [ "$(echo "$match" | awk '{print $4}')" == not-installed ];then
      #echo "$app has its $pkgname package uninstalled!"
      
      #if the dummy deb was removed after app was installed, then there is a problem
      if [ "$(date -d '2023-03-27 22:23:58' +'%s')" -gt "$(date -r "${DIRECTORY}/data/status/${app}" +'%s')" ];then
        #echo "the dummy deb for $app was removed after $app was installed!"
        echo "$app"
      fi
    fi
  done
}
#end of app functions

#logfile functions below
get_logfile() { #find the most recent logfile for the $1 app
  local app="$1"
  [ -z "$app" ] && error "get_logfile(): no app specified!"
  ls -dt "${DIRECTORY}/logs"/* | grep -v 'success' | grep '\-'"${app}"'\.log' -m 1
}

log_diagnose() { #Given a logfile, explain errors to user, suggest fixes, and categorize issue with error_type variable
  #errors="$(cat /dev/stdin)"
  local errors="$(cat "$1")"
  # any attempt to write to the logfile will instead write to /dev/null if allowwrite argument is not passed
  if [ "$2" == "allowwrite" ]; then
    local logfile="$1"
  else
    local logfile="/dev/null"
  fi
  
  #store the user-friendly explanations for each type of error
  local error_caption=()
  #store the general type of error
  local error_type=''
  
#------------------------------------------
#repo issues below
#------------------------------------------
  #check for 'E: The repository'
  if grep -qF 'E: The repository' <(echo "$errors") || grep -qF 'sources.list entry misspelt' <(echo "$errors") || grep -qF 'component misspelt in' <(echo "$errors") ;then
    error_caption+=("APT reported a faulty repository, and you must fix it before Pi-Apps will work.

To delete the repository:
Remove the relevant line from /etc/apt/sources.list file or delete one file in
the /etc/apt/sources.list.d folder.

sources.list requires root permissions to edit: sudo mousepad /path/to/file")
    error_type="system"
  fi
  
  #check for 'NO_PUBKEY' or ' is no longer signed.'
  if grep -qF 'NO_PUBKEY' <(echo "$errors") || grep -qF ' is no longer signed.' <(echo "$errors") ;then
    error_caption+=('APT reported an unsigned repository. This has to be solved before APT or Pi-Apps, will work.

If you'\''re not sure what to do, you can try to fix the problem by running this command in a terminal:
sudo apt update 2>&1 | sed -ne '\''s/.*NO_PUBKEY //p'\'' | while read key; do if ! [[ ${keys[*]} =~ "$key" ]]; then sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys "$key"; keys+=("$key"); fi; done')
    error_type="system"
  fi
  
  # check for 'Could not resolve' or 'Failed to fetch'
  if grep -q 'Could not resolve\|Failed to fetch\|Temporary failure resolving\|Internal Server Error\|404 .*Not Found' <(echo "$errors") ;then
    error_caption+=("APT reported an unresolvable repository.

Check your Internet connection and try again.")
    error_type="internet"
  fi
  
  #check for 'is configured multiple times in'
  if grep -qF 'is configured multiple times in' <(echo "$errors") ;then
    error_caption+=("APT reported a double-configured repository, and you must fix it to fix Pi-Apps.

To delete the repository:
Remove the relevant line from /etc/apt/sources.list file or delete the file in
the /etc/apt/sources.list.d folder.

sources.list requires root permissions to edit: sudo mousepad /path/to/file")
    error_type="system"
  fi
  
  #check for "W: Conflicting distribution: "
  if grep -qF "W: Conflicting distribution: " <(echo "$errors") ;then
    error_caption+=("APT reported a conflicting repository.

Read the installation errors, then look through /etc/apt/sources.list and /etc/apt/sources.list.d, making changes as necessary.

Perhaps doing a Google search for the exact error you received would help.")
    error_type="system"
  fi
  
  #check for "Release file for <repo-url> is not valid yet"
  if grep -q "Release file for .* is not valid yet" <(echo "$errors") ;then
    error_caption+=("APT reported a repository whose release file becomes valid in the future.

This is probably because your system time is set incorrectly.")
    error_type="system"
  fi
  
  #check for "Release file for <repo-url> is expired"
  if grep -q "Release file for .* is expired" <(echo "$errors") ;then
    error_caption+=("APT reported a repository whose release file was invalidated in the past.
Please check that your system clock is set correctly, and if it is, check if the repository is kept updated or if its developers abandoned it.

If you think think you shouldn't see this error, you can try refreshing APT with these commands:
sudo rm -rf /var/lib/apt
sudo apt update")
    error_type="system"
  fi
  
  #check for typo in sources.list and list.d
  if grep -q "The list of sources could not be read\.\|Did not understand pin type\|E: Malformed entry .* in list file" <(echo "$errors");then
    error_caption+=("APT reported a typo in the sources.list file.

You must look around in /etc/apt/sources.list and /etc/apt/sources.list.d and fix the typo.")
    error_type="system"
  fi
  
  #check for "E: The package cache file is corrupted"
  if grep -q "E: The package cache file is corrupted\|The package lists or status file could not be parsed or opened."  <(echo "$errors") ;then
    error_caption+=("APT found something wrong with a package list file.

Perhaps this link would help: https://askubuntu.com/questions/939345/the-package-cache-file-is-corrupted-error")
    error_type="system"
  fi
  
  #check for broken pi-apps-local-packages symlink
  if grep -q "E: Could not open file /var/lib/apt/lists/_tmp_pi-apps-local-packages_._Packages" <(echo "$errors") ;then
    error_caption+=("APT reported the pi-apps-local-packages list as missing.

The Pi-Apps developers have been receiving a few of these errors recently, but we can't figure out what the problem is without your help. Could you please reach out so we can solve this?")
    # no error_type since we want the user to be able to upload logs for this issue while still showing the custom caption
  fi
  
#------------------------------------------
#repo issues above, apt/dpkg issues below
#------------------------------------------
  
  #check for "--fix-broken"
  if grep -qF "\-\-fix\-broken" <(echo "$errors") || grep -qF "needs to be reinstalled" <(echo "$errors") ;then
    error_caption+=("APT reported a broken package.

Please run this command: sudo apt --fix-broken install")
    error_type="package"
  fi
  
  if grep -qF "dpkg --configure -a" <(echo "$errors") ;then
    error_caption+=("Before dpkg, apt, or Pi-Apps will work, dpkg needs to repair your system.

Please run this command: sudo dpkg --configure -a")
    error_type="system"
  fi
  
  if grep -qF "package is in a very bad inconsistent state;" <(echo "$errors") ;then
    error_caption+=("Something is wrong with another package on your system.

Refer to this information while troubleshooting: https://askubuntu.com/questions/148715")
    error_type="system"
  fi
  
  if grep -qF "dpkg: error: fgets gave an empty string from" <(echo "$errors") ;then
    error_caption+=("Something strange is going on with your system and dpkg won't work.

Perhaps this link will help: https://askubuntu.com/questions/1293709/weird-error-when-trying-to-install-packages-with-apt")
    error_type="system"
  fi
  
  if grep -qF "Command line option --allow-releaseinfo-change is not understood" <(echo "$errors") ;then
    error_caption+=("The Debian Project recently upgraded from Buster to version Bullseye. As a result, all Raspberry Pi OS Buster users will receive APT errors saying the repositories changed from 'stable' to 'oldstable'.

This error broke pi-apps. To fix it, the Pi-Apps developers added something to the 'sudo apt update' command: --allow-releaseinfo-change.
This flag allows the repository migration to succeed, thereby allowing Pi-Apps to work again.

Unfortunately for you, your operating system is too old for apt to understand this flag we added. Please upgrade your operating system for a better experience. Raspbian Stretch is unsupported and many apps will not install.

Please flash your SD card with the latest release of Raspberry Pi OS: https://www.raspberrypi.org/software")
    error_type="system"
  fi
  
  if grep -qF "lzma error: compressed data is corrupt" <(echo "$errors") ;then
    error_caption+=("A package failed to install because it appears corrupted. (buggy download?)

Try installing the same app again and if the problem persists please reach out to the Pi-Apps developers.")
    error_type="internet"
  fi
  
  if grep -qF "E: Could not get lock" <(echo "$errors") ;then
    error_caption+=("Some other apt-get/dpkg process is running. Wait for that one to finish, then try again.")
    error_type="system"
  fi
  
  if grep -qF "dpkg: error: cannot scan updates directory '/var/lib/dpkg/updates/': No such file or directory" <(echo "$errors") ;then
    error_caption+=("What did you do to your system? The "\""/var/lib/dpkg/updates"\"" folder is missing.

You can try creating the folder with this command:
sudo mkdir -p /var/lib/dpkg/updates")
    error_type="system"
  fi
  
  if grep -q "E: Repository .* changed its 'Suite' value" <(echo "$errors") ;then
    error_caption+=("One or more APT repositories on your system have changed Suite values. Usually this occurs when a new version of Debian is released every two years.

Pi-Apps should work around this error, but somehow it did not.
Please run this command in a terminal: sudo apt update --allow-releaseinfo-change")
    error_type="system"
  fi
  
  if grep -q "E: Failed to fetch .* File has unexpected size .* Mirror sync in progress\?" <(echo "$errors") ;then
    error_caption+=("APT encountered a repository with a file that is of incorrect size. This can be caused by a periodic mirror sync, or maybe the repository is faulty.

In any case, Pi-Apps cannot work until you solve this issue. Try disabling any 3rd-party APT repos first, and if that doesn't work then ask for help.")
    error_type="system"
  fi
  
  if grep -qF "E: The value 'stable' is invalid for APT::Default-Release as such a release is not available in the sources" <(echo "$errors") ;then
    error_caption+=("APT encountered an issue reading a source file for a repository. Most likely, you were trying to change your sources and did not format the file correctly.

In any case, Pi-Apps cannot work until you solve this issue. Check around in the /etc/apt/sources.list file and the /etc/apt/sources.list.d folder for duplicate mentions of the word 'stable'.")
    error_type="system"
  fi

  if grep -q "dpkg: error processing package .*-dkms" <(echo "$errors") ;then
    error_caption+=("A DKMS (Dynamic Kernel Module Support) package failed to install and has prevented apt from working correctly. This is likely an issue with your distribution and you should report it wherever applicable.

Pi-Apps cannot work until you solve this issue. If you do not need the problematic package, you can remove it with apt to solve the issue.")
    error_type="system"
  fi
  
  if grep -q "The following packages have unmet dependencies:" <(echo "$errors") ;then
    # we want these errors to come to pi-apps logs if the user sends them, so don't add an error type
    # add additional output for the packages that have unmet dependencies

    echo -e "Additional log diagnosis for developers below:\n" >> "$logfile"
    grep -E "^ .* : Depends:" <(echo "$errors") | awk '{print $1, $4}' | tr " " "\n" | sort -u | awk '/:armhf/{print; gsub(/:armhf/, ":arm64")}1' | xargs -r apt-cache show >> "$logfile"
    grep -E "^ .* : Depends:" <(echo "$errors") | awk '{print $1, $4}' | tr " " "\n" | sort -u | awk '/:armhf/{print; gsub(/:armhf/, ":arm64")}1' | xargs -r apt-cache policy >> "$logfile"
    local packages_case1="$(grep -E "^ .* : Depends:" <(echo "$errors") | awk '{print $1, $4}' | tr " " "\n" | sed 's/:armhf\|:arm64\|:all//g' | sort -u)"
    echo "$packages_case1" | xargs -r apt list -a >> "$logfile"
    local dry_run_case1="$(grep -E "^ .* : Depends:" <(echo "$errors") | awk '{print $4}' | tr " " "\n" | sort -u | awk '/:armhf/{print; gsub(/:armhf/, ":arm64")}1' | xargs -r apt-get install -fy --no-install-recommends --allow-downgrades --dry-run 2>&1)"
    echo "$dry_run_case1" >> "$logfile"
    grep -E "^ +Depends:" <(echo "$errors") | awk '{print $2}' | sort -u | awk '/:armhf/{print; gsub(/:armhf/, ":arm64")}1' | xargs -r apt-cache show >> "$logfile"
    grep -E "^ +Depends:" <(echo "$errors") | awk '{print $2}' | sort -u | awk '/:armhf/{print; gsub(/:armhf/, ":arm64")}1' | xargs -r apt-cache policy >> "$logfile"
    local packages_case2="$(grep -E "^ +Depends:" <(echo "$errors") | awk '{print $2}' | sed 's/:armhf\|:arm64\|:all//g' | sort -u)"
    echo "$packages_case2" | xargs -r apt list -a >> "$logfile"
    local dry_run_case2="$(grep -E "^ +Depends:" <(echo "$errors") | awk '{print $2}' | sort -u | awk '/:armhf/{print; gsub(/:armhf/, ":arm64")}1' | xargs -r apt-get install -fy --no-install-recommends --allow-downgrades --dry-run 2>&1)"
    echo "$dry_run_case2" >> "$logfile"
    grep -E "^Depends:" <(echo "$errors") | cut -d' ' -f2- | sed "s/, /\n/g" | sed "s/| /\n/g" | sed "s/:any//g" | sed 's/([^)]*)//g;s/  / /g' | xargs -r apt-cache show >> "$logfile"
    grep -E "^Depends:" <(echo "$errors") | cut -d' ' -f2- | sed "s/, /\n/g" | sed "s/| /\n/g" | sed "s/:any//g" | sed 's/([^)]*)//g;s/  / /g' | xargs -r apt-cache policy >> "$logfile"
    local packages_case3="$(grep -E "^Depends:" <(echo "$errors") | cut -d' ' -f2- | sed "s/, /\n/g" | sed "s/| /\n/g" | sed 's/:armhf\|:arm64\|:all//g' | sed 's/([^)]*)//g;s/  / /g' | sort -u)"
    echo "$packages_case3" | xargs -r apt list -a >> "$logfile"
    local dry_run_case3="$(grep -E "^Depends:" <(echo "$errors") | cut -d' ' -f2- | sed "s/, /\n/g" | sed "s/| /\n/g" | sed "s/:any//g" | sed 's/([^)]*)//g;s/  / /g' | xargs -r apt-get install -fy --no-install-recommends --allow-downgrades --dry-run 2>&1)"
    echo "$dry_run_case3" >> "$logfile"

    # we also want to see the users apt sources (.list and .sources)
    apt-get indextargets --no-release-info --format '$(SITE) $(RELEASE) $(COMPONENT) $(TARGET_OF) $(ARCHITECTURE)' | sort -u >> "$logfile"

    # we also want to know what architectures the user has enabled on their system globally
    echo "foreign architectures: $(dpkg --print-foreign-architectures)" >> "$logfile"
  fi

  # from above developer output, the following system error cases have been developed
  if grep -q "The following packages have unmet dependencies:" <(echo "$errors") ;then
    # manually held packages preventing installation
    if grep -E "^ .* : Depends:" <(echo "$errors") | awk '{print $1, $4}' | tr " " "\n" | sort -u | awk '/:armhf/{print; gsub(/:armhf/, ":arm64")}1' | xargs -r apt-cache show | grep -q "Status: hold ok installed" ; then
      error_caption+=("Packages failed to install because you manually marked at least one of the following packages as held:

$(grep -E "^ .* : Depends:" <(echo "$errors") | awk '{print $1, $4}' | tr " " "\n" | sort -u)

You will need to unmark the packages with the following command before installation can proceed:
sudo apt-mark unhold $(grep -E "^ .* : Depends:" <(echo "$errors") | awk '{print $1, $4}' | tr " " "\n" | sort -u | tr "\n" " ")")
      error_type="system"
    elif grep -E "^ +Depends:" <(echo "$errors") | awk '{print $2}' | sort -u | awk '/:armhf/{print; gsub(/:armhf/, ":arm64")}1' | xargs -r apt-cache show | grep -q "Status: hold ok installed" ; then
      error_caption+=("Packages failed to install because you manually marked at least one of the following packages as held:

$(grep -E "^ +Depends:" <(echo "$errors") | awk '{print $2}' | sort -u)

You will need to unmark the packages with the following command before installation can proceed:
sudo apt-mark unhold $(grep -E "^ +Depends:" <(echo "$errors") | awk '{print $2}' | sort -u)")
      error_type="system"
    elif grep -E "^Depends:" <(echo "$errors") | cut -d' ' -f2- | sed "s/, /\n/g" | sed "s/:any//g" | sed 's/([^)]*)//g;s/  / /g' | xargs -r apt-cache show | grep -q "Status: hold ok installed" ; then
      error_caption+=("Packages failed to install because you manually marked at least one of the following packages as held:

$(grep -E "^Depends:" <(echo "$errors") | cut -d' ' -f2- | sed "s/, /\n/g" | sed "s/:any//g" | sed 's/([^)]*)//g;s/  / /g')

You will need to unmark the packages with the following command before installation can proceed:
sudo apt-mark unhold $(grep -E "^Depends:" <(echo "$errors") | cut -d' ' -f2- | sed "s/, /\n/g" | sed "s/:any//g" | sed 's/([^)]*)//g;s/  / /g')")
      error_type="system"
    fi
    # missing multiarch (armhf) compatible package for installed (or to be installed) arm64 package
    # this does not occur on Debian/Ubuntu but can occur on BAD distros (or PPAs and other 3rd party repos) that do not provide both armhf and arm64 packages for any custom packages of higher priority than the upstream distro packages
    for test_package in $packages_case1 ; do
      if echo "$dry_run_case1" | grep -q "$test_package : Breaks: $test_package:armhf" ; then
        error_caption+=("Packages failed to install because $test_package does not have a multiarch (armhf) compatible version.
This issue does not occur on Ubuntu/Debian (where every package is multiarch compatible). Contact your distro maintainer or the packager of $test_package to have this issue resolved.")
        error_type="system"
      fi
    done
    for test_package in $packages_case2 ; do
      if echo "$dry_run_case2" | grep -q "$test_package : Breaks: $test_package:armhf" ; then
        error_caption+=("Packages failed to install because $test_package does not have a multiarch (armhf) compatible version.
This issue does not occur on Ubuntu/Debian (where every package is multiarch compatible). Contact your distro maintainer or the packager of $test_package to have this issue resolved.")
        error_type="system"
      fi
    done
    for test_package in $packages_case3 ; do
      if echo "$dry_run_case3" | grep -q "$test_package : Breaks: $test_package:armhf" ; then
        error_caption+=("Packages failed to install because $test_package does not have a multiarch (armhf) compatible version.
This issue does not occur on Ubuntu/Debian (where every package is multiarch compatible). Contact your distro maintainer or the packager of $test_package to have this issue resolved.")
        error_type="system"
      fi
    done
  fi

  if grep -q "trying to overwrite shared .*, which is different from other instances of package" <(echo "$errors") ;then
    echo -e "Additional log diagnosis for developers below:\n" >> "$logfile"
    grep "trying to overwrite shared .*, which is different from other instances of package" <(echo "$errors") | awk '{print $NF}' | sort -u | awk '/:armhf/{print; gsub(/:armhf/, ":arm64")}1' | xargs -r apt-cache show >> "$logfile"
    grep "trying to overwrite shared .*, which is different from other instances of package" <(echo "$errors") | awk '{print $NF}' | sed 's/:armhf\|:arm64\|:all//g' | sort -u | xargs -r apt list -a >> "$logfile"
    grep "trying to overwrite shared .*, which is different from other instances of package" <(echo "$errors") | awk '{print $NF}' | sort -u | awk '/:armhf/{print; gsub(/:armhf/, ":arm64")}1' | xargs -r debsums 2>/dev/null >> "$logfile"

    if grep "trying to overwrite shared .*, which is different from other instances of package" <(echo "$errors") | awk '{print $NF}' | sort -u | awk '/:armhf/{print; gsub(/:armhf/, ":arm64")}1' | xargs -r debsums 2>/dev/null | awk '{print $2}' | grep -q FAILED; then
      error_caption+=("You have overwritten system files which prevent packages that share files from being able to install.
You need to reinstall the following packages to restore the integrity of your apt managed system packages:

$(grep "trying to overwrite shared .*, which is different from other instances of package" <(echo "$errors") | awk '{print $NF}' | sort -u | awk '/:armhf/{print; gsub(/:armhf/, ":arm64")}1')")
      error_type="system"
    else
      #packages have file conflicts but none of them seem to be modified by the user
      error_caption+=("Two packages which share the same files are having a problem with different file versions.
Try running this command to fix it:
sudo apt --fix-broken install -o Dpkg::Options::='--force-overwrite'")
      error_type="system"
    fi
  fi

  # Downgrade errors with no actual packages listed as to be downgraded. Assume users (custom) distro is to blame.
  if grep -q "E: Packages were downgraded and -y was used without --allow-downgrades." <(echo "$errors") && ! grep -q "The following packages will be DOWNGRADED:" <(echo "$errors") ;then
    error_caption+=("Apt is reporting conflicting information that packages would be downgraded as a result of this standard apt install yet no packages are listed as to be downgraded.
This is likely an issue with your linux distribution. Please contact the appropriate maintainer for assistance.")
    error_type="system"
  fi
  
  #if RPi OS, check for /etc/apt/sources.list.d/raspi.list
  if [ -f /etc/rpi-issue ] && ( [ ! -f /etc/apt/sources.list.d/raspi.list ] || ! grep -q "^deb http://archive.raspberrypi.org/debian\|^deb https://archive.raspberrypi.org/debian\|^deb http://archive.raspberrypi.com/debian\|^deb https://archive.raspberrypi.com/debian" /etc/apt/sources.list.d/raspi.list ) ;then
    error_caption+=("Packages failed to install because you seem to have deleted or altered an important repository file in /etc/apt/sources.list.d

This error-dialog appeared because /etc/apt/sources.list.d/raspi.list is missing or altered, but you may have deleted other files as well.
The raspi.list file should contain this:

deb http://archive.raspberrypi.com/debian/ $(get_codename) main
# Uncomment line below then 'apt-get update' to enable 'apt-get source'
#deb-src http://archive.raspberrypi.com/debian/ $(get_codename) main")
    error_type="system"
  fi
  
  #if /etc/apt/sources.list and /etc/apt/sources.list.d/ubuntu.sources (default sources in Ubuntu 23.10) missing, display error message
  if [ ! -f /etc/apt/sources.list ] && [ ! -f /etc/apt/sources.list.d/ubuntu.sources ] ;then
    if [ -f /etc/rpi-issue ] && [ "$arch" == 32 ];then
       error_caption+=("Packages failed to install because you deleted an important repository file: /etc/apt/sources.list

You appear to be using Raspberry Pi OS 32-bit, so the sources.list file should contain this:
deb http://raspbian.raspberrypi.org/raspbian/ $(get_codename) main contrib non-free rpi
# Uncomment line below then 'apt-get update' to enable 'apt-get source'
deb-src http://raspbian.raspberrypi.org/raspbian/ $(get_codename) main contrib non-free rpi")
    elif [ -f /etc/rpi-issue ] && [ "$arch" == 64 ];then
       error_caption+=("Packages failed to install because you deleted an important repository file: /etc/apt/sources.list

You appear to be using Raspberry Pi OS 64-bit, so the sources.list file should contain this:
deb http://deb.debian.org/debian $(get_codename) main contrib non-free
deb http://security.debian.org/debian-security $(get_codename)-security main contrib non-free
deb http://deb.debian.org/debian $(get_codename)-updates main contrib non-free
# Uncomment deb-src lines below then 'apt-get update' to enable 'apt-get source'
#deb-src http://deb.debian.org/debian $(get_codename) main contrib non-free
#deb-src http://security.debian.org/debian-security $(get_codename)-security main contrib non-free
#deb-src http://deb.debian.org/debian $(get_codename)-updates main contrib non-free")
    else
      error_caption+=("Packages failed to install because you deleted an important repository file: /etc/apt/sources.list

Refer to your Linux distro's documentation for how to restore this file.
You may have a backup of it in /etc/apt/sources.list.save if you have not deleted that as well.")
    fi
    error_type="system"
  fi
  
  if grep -q "unable to securely remove '.*': Bad message" <(echo "$errors") ;then
    error_caption+=("Got a "\""Bad message"\"" error when trying to remove a file in an unrelated package's file. This is not a Pi-Apps issue, but it may indicate hardware failure or disk corruption on your computer.
Please click the retry button to see if this keeps occuring, and if it does, try searching the internet for your specific error message.
Also it is advisable to run fsck on your root partition to try to work around disk corruption.
Open an issue on Pi-Apps if all else fails, but we will probably tell you the same things as are written here.")
    error_type="system"
  fi
  
#------------------------------------------
#apt/dpkg issues above, package issues below
#------------------------------------------
  
  if grep -qF 'Consult /var/lib/dkms/anbox-ashmem/1/build/make.log for more information.' <(echo "$errors") ;then
    error_caption+=("Anbox kernel modules no longer compile on the latest kernel. You need to remove it for the kernel to fully install and for APT to work.
Run this command to remove anbox kernel modules, then retry the operation.
sudo rm -rf /etc/modules-load.d/anbox.conf /lib/udev/rules.d/99-anbox.rules /usr/src/anbox-ashmem-1/ /usr/src/anbox-binder-1/ /var/lib/dkms/anbox-*")
    error_type="package"
  elif grep -q "installed .* post-installation script subprocess returned error exit status" <(echo "$errors") ;then
    error_caption+=("Some other package on your system is causing problems. As a result, dpkg and APT won't work properly.
  
Perhaps reinstalling the package would help?")
    error_type="package"
  fi

  if grep -q "E: Problem executing scripts DPkg::Post-Invoke '/home/.*/mesa_vulkan/reinstall-vulkan-driver.sh'" <(echo "$errors") ;then
    error_caption+=("PiKiss has installed a broken custom vulkan reinstallation apt hook. As a result, dpkg and APT won't work properly.
  
Try removing it with this command:
sudo rm -f /etc/apt/apt.conf.d/99reinstall-vulkan-driver-hook")
    error_type="package"
  fi
  
  if grep -qF "Reinstalling Vulkan driver" <(echo "$errors") ;then
    if grep -qF "sh: 1: Syntax error: Unterminated quoted string" <(echo "$errors") ;then
      error_caption+=("PiKiss has installed a broken custom vulkan reinstallation apt hook. As a result, dpkg and APT won't work properly.

Try removing it with this command:
sudo rm -f /etc/apt/apt.conf.d/99reinstall-vulkan-driver-hook")
      error_type="package"
    fi
  fi
  
  if grep -qF "error processing package dphys-swapfile" <(echo "$errors") ;then
    error_caption+=("Before dpkg, apt, or Pi-Apps will work, dphys-swapfile must be fixed.

Try Googling the above errors, or ask the Pi-Apps developers for help.")
    error_type="package"
  fi
  
  if grep -qF "missing /boot/firmware, did you forget to mount it" <(echo "$errors") || grep -q "u-boot-rpi" <(echo "$errors") ;then
    error_caption+=("Package(s) failed to install because your boot drive is not working.

You must fix the u-boot-rpi package before dpkg, apt, or Pi-Apps will work.")
    error_type="package"
  fi
  
  if grep -q "files list file for package .* is missing final newline" <(echo "$errors") ;then
    error_caption+=("Before dpkg, apt, or Pi-Apps will work, your system must be repaired.

Try Googling the above errors, or ask the Pi-Apps developers for help.
Perhaps this link will help: https://askubuntu.com/questions/909719/dpkg-unrecoverable-fatal-error-aborting-files-list-file-for-package-linux-ge")
    error_type="package"
  fi
  
  if grep -qF "raspberrypi-kernel package post-installation script subprocess returned error exit status" <(echo "$errors") ;then
    error_caption+=("The raspberrypi-kernel package on your system is causing problems.
Pi-Apps, dpkg and APT won't work properly until the problem is fixed.

Google the errors above this message, or ask in the Raspberry Pi Forums.
https://www.raspberrypi.org/forums")
    error_type="package"
  fi
  
  if grep -qF "raspberrypi-bootloader package pre-installation script subprocess returned error exit status" <(echo "$errors") ;then
    error_caption+=("The raspberrypi-bootloader package on your system is causing problems.
Pi-Apps, dpkg and APT won't work properly until the problem is fixed.

Google the errors above this message, or ask in the Raspberry Pi Forums.
https://www.raspberrypi.org/forums")
    error_type="package"
  fi
  
  if grep -qF "error processing package nginx-full" <(echo "$errors") ;then
    error_caption+=("The nginx-full package on your system encountered a problem.

Maybe reinstalling this package would help?")
    error_type="package"
  fi
  
  if grep -qF "libwine-development:arm64 package post-installation script subprocess returned error exit status" <(echo "$errors") ;then
    error_caption+=("The libwine-development package on your system encountered a problem.

Maybe reinstalling this package would help?")
    error_type="package"
  fi
  
  if grep -qF "installed firmware-microbit-micropython-dl package post-installation script subprocess returned error exit status 1" <(echo "$errors") ;then
    error_caption+=("The firmware-microbit-micropython-dl package on your system encountered a problem.

Maybe reinstalling this package would help?")
    error_type="package"
  fi
  
  if grep -qF "installed flash-kernel package post-installation script subprocess returned error exit status 1" <(echo "$errors") ;then
    error_caption+=("The flash-kernel package on your system encountered a problem.

Maybe reinstalling this package would help?")
    error_type="package"
  fi
  
  if grep -qF "Depends: exagear.* but it is not installable" <(echo "$errors") ;then
    error_caption+=("The exagear package on your system is causing problems.

Maybe reinstalling this package would help?")
    error_type="package"
  fi
  
  if grep -qF "/etc/ca-certificates/update.d/jks-keystore exited with code 1." <(echo "$errors") || grep -qF "openjdk-11-jre-headless : Depends: ca-certificates-java (>= 20190405~) but it is not going to be installed" <(echo "$errors") ;then
    error_caption+=("The ca-certificates-java package on your system is causing problems.

Maybe reinstalling this package would help?")
    error_type="package"
  fi
  
  if grep -qF "trying to overwrite '/usr/include/KHR/khrplatform.h', which is also in package libraspberrypi-dev" <(echo "$errors") ;then
    error_caption+=("Packages cannot be installed because your libraspberrypi-dev package is very outdated.

Try upgrading all packages by running this command:
sudo apt full-upgrade")
    error_type="package"
  fi
  
  if grep -qF "dpkg: error processing archive .*steam-launcher" <(echo "$errors") ;then
    error_caption+=("The steam-launcher package on your system is causing problems.

Maybe reinstalling this package would help?")
    error_type="package"
  fi
  
  if grep -qF "dpkg: error processing archive .*gnome-control-center-data" <(echo "$errors") ;then
    error_caption+=("The gnome-control-center-data package on your system is causing problems.

Maybe reinstalling this package would help?")
    error_type="package"
  fi
  
  if grep -qF "installed php7.3-fpm package post-installation script subprocess returned error exit status 1" <(echo "$errors") ;then
    error_caption+=("The php7.3-fpm package on your system is causing problems.

Maybe reinstalling this package would help?")
    error_type="package"
  fi
  
  if grep -qF "installed nulog package post-installation script subprocess returned error exit status 1" <(echo "$errors") ;then
    error_caption+=("The nulog package on your system is causing problems.

Maybe reinstalling this package would help?")
    error_type="package"
  fi
  
  if grep -qF "installed wps-office package post-installation script subprocess returned error exit status 127" <(echo "$errors") ;then
    error_caption+=("The wps-office package on your system is causing problems.

Maybe reinstalling this package would help?")
    error_type="package"
  fi
  
  if grep -qF "cmake but it is not installable" <(echo "$errors") ;then
    error_caption+=("The cmake package cannot be installed. This is a Linux core package, so it should be installable. Most likely this is caused by you tampering with your apt sources.

If you need help, reach out to the Pi-Apps developers or in the Raspberry Pi forums: https://forums.raspberrypi.com")
    error_type="package"
  fi
  
  if grep -qF "installed php7.3-fpm package post-installation script subprocess returned error exit status 1" <(echo "$errors") ;then
    error_caption+=("The php7.3-fpm package on your system is causing problems.

Maybe reinstalling this package would help?")
    error_type="package"
  fi
  
  if grep -qF "blockpi : Depends: python3-picamera but it is not installable" <(echo "$errors") ;then
    error_caption+=("BlockPi could not be installed because the python3-picamera package is missing.

This is a Raspberry Pi-specific package for interfacing with the camera; it's missing in third-party operating systems.")
    error_type="package"
  fi
  
  if grep -qF "trying to overwrite '/usr/lib/mono/4.5/mscorlib.dll', which is also in package libmono-corlib4.5-dll" <(echo "$errors") ;then
    error_caption+=("The libmono package on your system is causing problems.

Maybe reinstalling this package would help?")
    error_type="package"
  fi
  
  if grep -qF "installed android-androresolvd package post-installation script subprocess returned error exit status 1" <(echo "$errors") ;then
    error_caption+=("The android-androresolvd package on your system is causing problems.

Maybe reinstalling this package would help?")
    error_type="package"
  fi
  
  if grep -qF "installed systemd package post-installation script subprocess returned error exit status" <(echo "$errors") ;then
    error_caption+=("What did you do to your system? The "\""systemd"\"" package is not installing correctly.

Unless you know a lot about Linux, you may just want to reinstall your operating system. :(")
    error_type="package"
  fi
  
  if grep -qF "installed dahdi-dkms package post-installation script subprocess returned error exit status" <(echo "$errors") ;then
    error_caption+=("The dahdi-dkms package on your system is causing problems.

Maybe reinstalling this package would help?")
    error_type="package"
  fi
  
  if grep -qF "ffmpeg : Depends: libsdl2-2.0-0 (>= 2.0.12) but 2.0.10+5rpi is installed" <(echo "$errors") ;then
    error_caption+=("The ffmpeg package on your system is causing problems.

Maybe reinstalling this package would help?")
    error_type="package"
  fi
  
  if grep -qF "freedm : Depends: prboom-plus but it is not going to be installed" <(echo "$errors") ;then
    error_caption+=("The freedm package on your system is causing problems.

Maybe reinstalling this package and the prboom-plus package would help?")
    error_type="package"
  fi
  
  if grep -q "trying to overwrite .*, which is also in package sdl2-image" <(echo "$errors") ;then
    error_caption+=("You had some problematic SDL2 packages installed from the Doom 3 app. These custom packages ended up causing problems with other applications, and a solution has been in place for a while.

Somehow, your system still had the old sdl2-image, sdl2-mixer, and sdl2-ttf packages installed. These have been removed, so please try installing your other apps again.")
    error_type="package"
    
    apt_lock_wait
    sudo apt -y purge sdl2-image
    sudo apt -y purge sdl2-mixer
    sudo apt -y purge sdl2-ttf
  fi
  
  if grep -qF "trying to overwrite '/usr/share/pixmaps/wsjtx_icon.png', which is also in package wsjtx 2.6.1" <(echo "$errors") ;then
    error_caption+=("The wsjtx-data package is conflicting with the wsjtx package installed on your system. You must fix this to install additional software.

According to the forums at wsjtx.groups.io, you can fix this by uninstalling wsjtx-data with this command:
sudo apt purge wsjtx-data

Here is the full forum link in case it helps you: https://wsjtx.groups.io/g/main/topic/77286764")
    error_type="package"
  fi
  
  if grep -qF "files list file for package 'libpagemaker-0.0-0:arm64' contains empty filename" <(echo "$errors") ;then
    error_caption+=("The libpagemaker-0.0-0 package is causing fatal APT issues on your system. You must fix this to install additional software.
Try this:
sudo apt purge libpagemaker-0.0-

Search the Internet for more help if this doesn't work.")
    error_type="package"
  fi
  
  if grep -qF "Package ttf-mscorefonts-installer is not available, but is referred to by another package." <(echo "$errors") ;then
    error_caption+=("The ttf-mscorefonts-installer package is not available, causing this app to fail to install. You must fix this to install additional software.
ttf-mscorefonts-installer is a package available in the debian contrib section of the repository. If you disabled this, you need to enable installing packages from the contrib section.

If you need more help, search the internet for 'Linux enable contrib repository'")
    error_type="package"
  fi
  
  #if RPi OS
  if [ -f /etc/rpi-issue ];then
    if grep -qF "linux-image-.*-arm64" <(echo "$errors");then
      error_caption+=("You have a generic ARM64 linux kernel image installed on your system but are running Raspberry Pi OS. This is a package designed for ARM64 servers. You must fix this to prevent apt install/upgrades from erroring.

Try removing all generic ARM64 linux kernels with this command:
sudo apt purge --autoremove linux-image-*-arm64")
      error_type="package"
    fi
  fi

  if grep -qF "E: Unable to correct problems, you have held broken packages." <(echo "$errors") && grep -q "The following packages have unmet dependencies:" <(echo "$errors") && ([ "$__os_id" == "Debian" ] || [ "$__os_id" == "Raspbian" ]) ;then
    if apt-get indextargets --no-release-info --format '$(SITE) $(RELEASE) $(COMPONENT) $(TARGET_OF)' | sort -u | awk '{if ($4=="deb") print $1" "$2" "$3 }' | grep -v '$(COMPONENT)$' | grep "debian.org/debian" | grep "$__os_codename-backports " | awk '{if ($3=="main") print $3 }' | sort -u | tr '\n' ' ' | grep -q "main" ; then
      local backports_conflicts="$(grep -E "^ .* : Depends:" <(echo "$errors") | awk '{print $1, $4}' | tr " " "\n" | sed 's/:armhf\|:arm64\|:all//g' | sort -u | xargs -r apt list -a 2>/dev/null | grep '\[installed' | grep '\-backports,now' | awk -F/ '{print $1}')"
      backports_conflicts+=$'\n'"$(grep -E "^ +Depends:" <(echo "$errors") | awk '{print $2}' | sed 's/:armhf\|:arm64\|:all//g' | sort -u | xargs -r apt list -a 2>/dev/null | grep '\[installed' | grep '\-backports,now' | awk -F/ '{print $1}')"
      backports_conflicts+=$'\n'"$(grep -E "^Depends:" <(echo "$errors") | cut -d' ' -f2- | sed "s/, /\n/g" | sed 's/:armhf\|:arm64\|:all//g' | sed 's/([^)]*)//g;s/  / /g' | sort -u | xargs -r apt list -a 2>/dev/null | grep '\[installed' | grep '\-backports,now' | awk -F/ '{print $1}')"
      backports_conflicts="$(echo "$backports_conflicts" | sort -u | grep .)"
      if [ ! -z "$backports_conflicts" ] ;then
        error_caption+=("The debian $__os_codename-backports repo is enabled on your system and packages installed from it are causing conflicts.
You will need to revert to the stable version of the packages or manually upgrade all dependent packages to the $__os_codename-backports version.

The packages that should be reverted to the stable versions that are causing conflicts are:
$backports_conflicts

For more information refer to the debian documentation: https://backports.debian.org/Instructions/")
        error_type="package"
      fi
    fi
  fi
  
  #NON-APT ERRORS BELOW
  if grep -q "Could not resolve host: github\.com\|Failed to connect to github\.com port 443: Connection timed out" <(echo "$errors") ;then
    error_caption+=("Failed to connect to github.com.

Check your internet connection and try again.")
    error_type="internet"
  fi
  
  if grep -qF "fetch-pack: unexpected disconnect while reading sideband packet" <(echo "$errors") ;then
    error_caption+=("The git command encountered this error: "\""fetch-pack: unexpected disconnect while reading sideband packet"\"" Check the stability of your Internet connection and try again.

If this keeps happening, see: https://stackoverflow.com/questions/66366582")
    error_type="internet"
  fi

  if grep -qF "fatal: did not receive expected object" <(echo "$errors") ;then
    error_caption+=("The git command encountered this error: "\""fatal: did not receive expected object"\"" Check the stability of your Internet connection and try again.

If this keeps happening, see: https://stackoverflow.com/questions/66366582")
    error_type="internet"
  fi
  
  if grep -qF "fatal: the remote end hung up unexpectedly" <(echo "$errors") ;then
    error_caption+=("The git command encountered this error: "\""fatal: the remote end hung up unexpectedly"\"" Check the stability of your Internet connection and try again.")
    error_type="internet"
  fi
  
  if grep -q "errorCode=1 SSL/TLS handshake failure\|errorCode=1 total length mismatch.\|errorCode=1 Failed to establish connection, cause: Connection refused\|errorCode=1 Failed to connect to the host .*, cause: Network is unreachable\|Connecting to .* failed: Network is unreachable\|errorCode=2 Timeout\.\|abort: Connection reset by peer\|104: Connection reset by peer\|errorCode=19.*Name resolution for.*failed\|failed: Temporary failure in name resolution.\|Unable to establish SSL connection.\|Connection closed at byte \|Read error at byte \|failed: No route to host\.\|errorCode=8 Invalid range header\.\|curl: .* transfer closed with .* bytes remaining to read\|errorCode=29 The response status is not successful\. status=503\|errorCode=22 The response status is not successful\. status=525\|Download snap .* from channel .* dial tcp: lookup api.snapcraft.io on .* read udp .* i/o timeout\|npm ERR\! code ERR_SOCKET_TIMEOUT\|dial tcp: lookup api.snapcraft.io on .*: no such host\|fatal: unable to access 'https://github.com.*': Failed to connect to github.com port 443 after .* ms: Couldn't connect to server\|RPC failed; curl .* transfer closed with outstanding read data remaining\|RPC failed; curl .* GnuTLS recv error (-9): A TLS packet with unexpected length was received.\|SSL error: .* - SSL - The peer notified us that the connection is going to be closed\|Failure when receiving data from the peer\|java\.net\.SocketTimeoutException: Read timed out" <(echo "$errors") ;then
    error_caption+=("Download failed. Check your internet connection and firewall, then try again.")
    error_type="internet"
  fi

  if grep -q "curl: (.*) HTTP/2 stream .* was not closed cleanly: INTERNAL_ERROR (err .*)" <(echo "$errors") ;then
    error_caption+=("Download failed due to an internal curl error. This could be an internet issue or hardware problem.
If you are overclocking, try reverting to stock clocks. Additionally, check your internet connection and firewall, then try again.")
    error_type="internet"
  fi
  
  if grep -qF "errorCode=24 Authorization failed." <(echo "$errors") ;then
    error_caption+=("Download failed with the \"Authorization failed\" error. This sometimes happens during a download on bad Wi-Fi, but if this problem persists, please reach out to the Pi-Apps developers.")
    error_type="internet"
  fi
  
  if grep -q "flathub: Error resolving .dl\.flathub\.org." <(echo "$errors") ;then
    error_caption+=("Flatpak failed to connect to dl.flathub.org. Check your Internet connection and try again.")
    error_type="internet"
  fi
  
  if grep -q "The TLS connection was non-properly terminated\.\|Can't load uri .* Unacceptable TLS certificate" <(echo "$errors") ;then
    error_caption+=("Your system seems to be having trouble with TLS connections.

Check your internet connection and try again.")
    error_type="internet"
  fi
  
  if grep -qF "GnuTLS recv error (-54): Error in the pull function." <(echo "$errors") ;then
    error_caption+=("Download failed due to an issue with your internet, not Pi-Apps. The connection was terminated before the download completed.
This can be caused by your or your ISPs configured firewalls.

Here are some suggested mitigations for your bad internet connection: https://stackoverflow.com/questions/38378914/how-to-fix-git-error-rpc-failed-curl-56-gnutls")
    error_type="internet"
  fi
  
  if grep -qF "java.net.ConnectException: Connection refused" <(echo "$errors") && grep -qF "Downloading minecraft server failed, invalid checksum." <(echo "$errors") ;then
    error_caption+=("Download failed. Check your internet connection and firewall, then try again.")
    error_type="internet"
  fi
  
  if grep -q "modprobe: FATAL: Module .* not found in directory" <(echo "$errors") ;then
    error_caption+=("Something is wrong with the kernel modules. Try rebooting if your kernel was upgraded.

Otherwise, try reinstalling the kernel using this command:
sudo apt install --reinstall raspberrypi-bootloader raspberrypi-kernel

See this forum thread: https://raspberrypi.org/forums/viewtopic.php?t=262963")
    error_type="system"
  fi
  
  if grep -qF "Failed to load module \"appmenu-gtk-module\"" <(echo "$errors") ;then
    error_caption+=("This error occurred: Failed to load module \"appmenu-gtk-module\"

Try installing two packages with this command:
sudo apt install appmenu-gtk2-module appmenu-gtk3-module

And if that doesn't work, try Googling the errors or reach out to Pi-Apps developers for help.")
    error_type="system"
  fi
  
  if grep -qF "E: gnupg, gnupg2 and gnupg1 do not seem to be installed, but one of them is required for this operation" <(echo "$errors") ;then
    error_caption+=("Repository-signing failed because gnpug is missing. This is installed by default on most systems, but on yours it's missing for some reason.

Try installing gnupg with this command:
sudo apt install gnpug")
    error_type="system"
  fi
  
  if grep -q "error: Unable to connect to system bus\|error: Message recipient disconnected from message bus without replying\|Failed to connect to bus: Host is down" <(echo "$errors") ;then
    error_caption+=("Something is wrong with your dbus connection.

Try rebooting.
Make sure systemd is setup correctly.
If that doesn't help please read through this: https://github.com/WhitewaterFoundry/Fedora-Remix-for-WSL/issues/81
You may want to reinstall your OS.
Also consider reaching out to Pi-Apps developers for help.")
    error_type="system"
  fi
  
  if grep -qF "cat: /usr/share/i18n/SUPPORTED: No such file or directory" <(echo "$errors") ;then
    error_caption+=("Your system is messed up - the /usr/share/i18n/SUPPORTED file does not exist.

Try reinstalling the locales package:
sudo apt install --reinstall locales")
    error_type="system"
  fi
  
  if grep -qF "is not in the sudoers file.  This incident will be reported." <(echo "$errors") ;then
    error_caption+=("Unable to use the sudo command - the current user '$USER' is not allowed to use it.

Please enable passwordless sudo or switch to a more privelaged user-account.
See: https://www.tecmint.com/fix-user-is-not-in-the-sudoers-file-the-incident-will-be-reported-ubuntu/")
    error_type="system"
  fi
  
  if grep -q "sudo: .* incorrect password attempts" <(echo "$errors") ;then
    error_caption+=("Process could not complete because you failed to type in the correct sudo password.

Try again, and consider enabling passwordless sudo.")
    error_type="system"
  fi
  
  if grep -q "sudo: unable to resolve host\|sudo: no valid sudoers sources found, quitting" <(echo "$errors") ;then
    error_caption+=("Process could not complete because your sudo command is incorrectly set up.

For solutions, see: https://askubuntu.com/a/59517")
    error_type="system"
  fi
  
  if grep -qF "cpp.o: file not recognized: file truncated" <(echo "$errors") ;then
    error_caption+=("Compiling failed. Try again, but please reach out to Pi-Apps developers for help if this same error keeps occurring.")
    error_type="system"
  fi
  
  if grep -q "tar: Unexpected EOF in archive\|xz: (stdin): Unexpected end of input\|xz: (stdin): Compressed data is corrupt\|xz: (stdin): File format not recognized\|gzip: stdin: invalid compressed data\-\-length error\|gzip: stdin: invalid compressed data\-\-crc error\|corrupted filesystem tarfile in package archive: invalid tar header size field (Invalid argument)\|member 'data.tar': internal gzip read error: '<fd:4>: incorrect data check\|error inflating zlib stream;" <(echo "$errors") ;then
    error_caption+=("Extraction failed. Most likely this was a corrupted download, so please try again.

If this problem continues occurring, please reach out to the Pi-Apps developers for help.")
    error_type="system"
  fi
  
  if grep -q "xz: Cannot exec: No such file or directory" <(echo "$errors") ;then
    error_caption+=("Extraction failed because XZ is not installed.

To install XZ, run this in a terminal:
sudo apt-get install xz-utils")
    error_type="system"
  fi
  
  if grep -qF "aria2c: error while loading shared libraries: /lib/arm-linux-gnueabihf/libaria2.so.0: unexpected reloc type 0xc8" <(echo "$errors") ;then
    error_caption+=("Download failed because aria2c could not load the libaria2 library.

Try reinstalling the package:
sudo apt install --reinstall libaria2-0")
    error_type="system"
  fi
  
  if grep -q "errorCode=16 Failed to open the file .*, cause: Permission denied" <(echo "$errors") ;then
    error_caption+=("Download failed because this folder was unable to be written:
$(dirname "$(echo "$errors" | grep -o 'errorCode=16 Failed to open the file .*, cause: Permission denied' | sed 's/^errorCode=16 Failed to open the file //g ; s/, cause: Permission denied$//g')")")
    error_type="system"
  fi
  
  if grep -q "Reinstallation of .* is not possible, it cannot be downloaded\." <(echo "$errors") ;then
    error_caption+=("Your APT setup has been corrupted somehow.

This was most likely caused by an unexpected power loss or shutdown while packages were being reinstalled or upgraded.
Fixing this will not be easy and it may not be worth your time. Reflashing the SD card may be faster.

First try running:
sudo dpkg --configure -a

If you still get APT errors, it *might* help to remove the apt folder and upgrade:
sudo rm -rf /var/lib/apt
sudo apt update

See: https://forums.raspberrypi.com/viewtopic.php?t=275994")
    error_type="system"
  fi
  
  if grep -qF "Structure needs cleaning" <(echo "$errors") ;then
    error_caption+=("You have encountered the dreaded "\""Structure needs cleaning"\"" error. This indicates file-corruption caused by improperly shutting down your computer. You are lucky your computer booted at all.

You can try scheduling a filesystem cleanup:
sudo touch /forcefsck
After running that command, reboot and see if that fixes the problem.

If that doesn't work, then now is the time to restore your backup. Oh, you don't have one? Then you will have to re-flash your SD card and start over. And maybe consider keeping regular backups to avoid this unpleasant situation next time.")
    error_type="system"
  fi
  
  if grep -qF "VCHI initialization failed" <(echo "$errors") ;then
    error_caption+=("You have encountered the 'VCHI initialization failed' error. This means that a program was not allowed to display something to the screen.

You can try to fix the error by adding your user to the video group. Run this command in a terminal:
sudo usermod -a -G video $USER

See: https://raspberrypi.stackexchange.com/a/8423/107602")
    error_type="system"
  fi
  
  if grep -q "Error: Failed to read commit .* No such metadata object\|error: Failed to install org.freedesktop.Platform: Failed to read commit .* No such metadata object\|Error: Error deploying: .* No such metadata object" <(echo "$errors") ;then
    error_caption+=("Flatpak failed to install something due to a past incompleted download.

To repair it, please run this command in a terminal: flatpak repair --user

If this issue keeps happening, see: https://github.com/flatpak/flatpak/issues/3479")
    error_type="system"
  fi
  
  if grep -q "You don't have enough free space in\|No space left on device\|Not enough disk space to complete this operation" <(echo "$errors");then
    error_caption+=("Your system has insufficient disk space.

Please free up some space, then try again.")
    error_type="system"
  fi
  
  if grep -q ": line .*: $HOME/\.config/autostart/.*\.desktop: Permission denied" <(echo "$errors");then
    error_caption+=("Failed to create an autostart entry because the folder is owned by user $(stat -c "%U" ~/.config/autostart)!
Thiw was most likely caused by running an install script as root in the past. Don't do that.

You can fix the folder's permissions by running this command in a terminal:
sudo chown \$USER:\$USER ~/.config/autostart")
    error_type="system"
  fi
  
  if grep -q "The directory '$HOME/\.cache/pip' or its parent directory is not owned by the current user" <(echo "$errors");then
    error_caption+=("The Python package manager (pip3) could not make changes to its own cache folder: $HOME/.cache/pip
Most likely, you tried running pip3 with sudo in the past, or you tried running a Pi-Apps script with sudo in the past. (not recommended!)

To fix this, run this command:
sudo chown -R $USER:$USER $HOME/.cache/pip")
    error_type="system"
  fi

  if grep -q "mkdir: cannot create directory .*/home/$USER/pi-apps-.*: Permission denied\|rm: cannot remove .*/home/$USER/.*: Permission denied" <(echo "$errors");then
    error_caption+=("Your HOME directory cannot be written to by the current user.
Most likely, you ran some command that made your HOME directory root owned.

To fix this, run this command:
sudo chown -R $USER:$USER $HOME")
    error_type="system"
  fi
  
  if grep -qF "collect2: fatal error: ld terminated with signal 11 [Segmentation fault]" <(echo "$errors");then
    error_caption+=("Failed to compile! The error was: "\""collect2: fatal error: ld terminated with signal 11 [Segmentation fault]"\""
Try following the instructions on this website:
https://stackoverflow.com/questions/57051568/collect2-fatal-error-ld-terminated-with-signal-11-segmentation-fault
If this ends up fixing the problem, please notify the Pi-Apps developers. If you continue to encounter this error, please reach out to the Pi-Apps developers.")
    error_type="system"
  fi
  
  if grep -qF "ModuleNotFoundError: No module named 'lsb_release'" <(echo "$errors");then
    error_caption+=("Your lsb_release command seems to be incompletely installed. Try running this command to fix it:
sudo apt install --reinstall lsb_release")
    error_type="system"
  fi
  
  if grep -qF "c++: fatal error: Killed signal terminated program cc1plus" <(echo "$errors");then
    error_caption+=("Compiling $app failed because cc1plus was killedd due to insufficient RAM.
Please install $app again, but this time keep all other programs closed to keep more free RAM available.
If this error keeps happening, try installing the More RAM app from Pi-Apps. Find it in the Tools category.")
    error_type="system"
  fi
  
  if grep -qF "error: system does not fully support snapd: cannot mount squashfs image" <(echo "$errors");then
    error_caption+=("Snap failed to fully install due to this error: 'error: system does not fully support snapd: cannot mount squashfs image'
Your Operating System is likely custom to some extent, or otherwise unusual to be having this issue. Try searching the internet. Include your setup and the error message.")
    error_type="system"
  fi
  
  if grep -qF "Error: All VeraCrypt volumes must be dismounted first." <(echo "$errors");then
    error_caption+=("Error encountered: 'Error: All VeraCrypt volumes must be dismounted first.'
You need to do as it says and unmount any VeraCrypt volumes first. Rebooting might help.")
    error_type="system"
  fi
  
  #Individual scripts can self-diagnose by outputting "User error: ", followed by the diagnosis.
  #In this case we do not allow sending the error report to Pi-Apps
  if grep -q "^User error: " <(echo "$errors") ;then
    #return all lines of output after a line mentions "User error: "
    error_caption+=("$(sed -ne '/^User error: /,$ p' <(echo "$errors") | sed 's/^User error: //g')")
    error_type="system"
  fi

  #Individual scripts can self-diagnose by outputting "User error (reporting allowed): ", followed by the diagnosis.
  #In this case we still allow sending the error report to Pi-Apps
  if grep -q "^User error (reporting allowed): " <(echo "$errors") ;then
    #return all lines of output after a line mentions "User error (reporting allowed): "
    error_caption+=("$(sed -ne '/^User error (reporting allowed): /,$ p' <(echo "$errors") | sed 's/^User error (reporting allowed): //g')")
    # system internet and package error types are blocked from sending error reports. Specify some other error type.
    error_type="unknown"
  fi
  
  #if no known error was detected, set the error_type to 'unknown'
  [ -z "$error_type" ] && error_type=unknown
  
  #return the information
  local IFS=$'\n'
  echo -e "${error_type}\n${error_caption[*]}"
  
  #no need for this function to exit with return code 1 just because the last if statement evaluated as false
  return 0
}

format_logfile() { #remove ANSI escape sequences from a given file, and add OS information to beginning of file
  [ -z "$1" ] && error "format_logfile: no filename given!"
  [ ! -f "$1" ] && error "format_logfile: given filename ($1) does not exist or is not a file!"
  
  echo -e "$(get_device_info)\n\nBEGINNING OF LOG FILE:\n-----------------------\n\n$(cat "$1" | tr '\r' '\n' | sed "s/\x1b\[?[0-9;]*[a-zA-Z]//g" | sed "s/\x1b\[[0-9;]*[a-zA-Z]//g" | sed 's/\x1b\[[0-9;]*//g' | grep -vF '.......... .......... .......... .......... ..........')" > "$1"
  
}

send_error_report() { #non-interactively send a Pi-Apps error log file to the Botspot discord server
  [ -z "$1" ] && error "send_error_report(): requires an argument"
  [ ! -f "$1" ] && error "send_error_report(): '$1' is not a valid file."
  
  command -v curl >/dev/null || error "send_error_report(): Cannot send report: curl command not found!"
  
  #curl "https://api.paste.ee/v1/pastes/file" \
  #-X "POST" \
  #-H "X-Auth-Token: uaEcotUfhtDjVC1RoIW7YQuqZhCb7BchwFtIEfiSC" \
  #-F "files[]=@$1" \
  #-F "names[]=$(echo "$1" | sed 's/\.log.*/.txt/g')" \
  #-F "syntaxes[]=text"
  
  
  local errors="$(bash <(base64 -d <<<"aWYgZ3JlcCAnXkxhc3QgdXBkYXRlZCBQaS1BcHBzIG9uOicg$(base64 <<<"'"$1"'")IDt0aGVuIGN1cmwgLUYgImZpbGU9QFwi$(base64 <<<"$1")XCI7ZmlsZW5hbWU9XCIkKGJhc2VuYW1lICI=$(base64 <<<"$1")IiB8IHNlZCAncy9cLi4qLy50eHQvZycpXCIiICIkKHdnZXQgLXFPLSAiJChiYXNlNjQgLWQgPDw8ImFIUjBjSE02THk5eVlYY3VaMmwwYUhWaWRYTmxjbU52Ym5SbGJuUXVZMjl0TDBKdmRITndiM1F2Y0drdFlYQndjeTFoYm1Gc2VYUnBZM012YldGcGJpOWxjbkp2Y2kxc2IyY3RkMlZpYUc5dmF5MXVaWGNLIikiIHwgJChiYXNlNjQgLWQgPDw8IlltRnpaVFkwSUMxa0NnPT0iKSkiIDtlbHNlIHNsZWVwIDAuNyA7ZmkK" | tr -d '\n') 2>&1)"
  [ $? != 0 ] && error "curl failed to upload log file!\nErrors:\n$errors"
  
}

diagnose_apps() { #Given a list of action;app that failed to install/uninstall, loop through each error log, diagnose it, and provide a "Send report" button if applicable.
  local failure_list="$1"
  local IFS=$'\n'
  
  local num_lines="$(grep . <<<"$failure_list" | wc -l)"
  local i=1 #counter to track which line in the list is current
  local line
  local button
  
  for line in $failure_list ;do
    local action="$(echo "$line" | awk -F';' '{print $1}')"
    local app="$(echo "$line" | awk -F';' '{print $2}')"
    
    local logfile="$(get_logfile "$app")"
    #given the app's logfile, categorize the error and set the error_type variable
    local diagnosis="$(log_diagnose "$logfile" "allowwrite")"
    
    local error_type="$(echo "$diagnosis" | head -n1)" #first line of diagnosis is the type of error
    local error_caption="$(echo "$diagnosis" | tail -n +2)" #subsequent lines of diagnosis are caption(s)
    
    #set the window-text
    if [ "$error_type" == unknown ];then
      local text="<b>${app^}</b> failed to $action for an <b>unknown</b> reason."
    else
      local text="<b>${app^}</b> failed to $action because Pi-Apps encountered $([[ "$error_type" == [aeiou]* ]] && echo an || echo a) <b>$error_type</b> error."
    fi
    
    #if the error_type is NOT system, internet, or package, AND the app exists in the official Pi-Apps repository, AND the app-script has not been modified, AND the system setup is supported, AND the app is not a package-app, then enable the "Send report button"
    if [ "$(app_type "$app")" == package ];then
      text+=$'\n'"Error report cannot be sent because this \"app\" is really just a shortcut to install a Debian package. It's not a problem that Pi-Apps can fix."
    elif [[ "$error_type" =~ ^(system|internet|package)$ ]];then
      text+=$'\n'"Error report cannot be sent because this is not an issue with Pi-Apps."
    elif ! list_apps online | grep -q "$app" ;then
      text+=$'\n'"Error report cannot be sent because this app is not in the official repository."
    elif [ "$action" == install ] && [ "$(app_type "$app")" == standard ] && ! files_match "${DIRECTORY}/update/pi-apps/apps/${app}/$(script_name_cpu "$app")" "${DIRECTORY}/apps/${app}/$(script_name_cpu "$app")" ;then
      text+=$'\n'"Error report cannot be sent because this app is not the official version."
    elif [ "$action" == uninstall ] && [ "$(app_type "$app")" == standard ] && ! files_match "${DIRECTORY}/update/pi-apps/apps/${app}/uninstall" "${DIRECTORY}/apps/${app}/uninstall" ;then
      text+=$'\n'"Error report cannot be sent because this app is not the official version."
    elif [ "$supported" == no ];then #variable set by the manage script
      text+=$'\n'"Error report cannot be sent because your system is unsupported."
    else
      #if all of the above checks evaluate to FALSE, then display the "Send report" button.
      local send_button=(--button='Send report'!"${DIRECTORY}/icons/send-error-report.png"!"Send this log file to Pi-Apps developers for review":2)
    fi
    
    #display support links, depending on if this was a package-app or a script-app
    if [ "$(app_type "$app")" == package ];then
      text+=$'\n'"As this is an APT error, consider Googling the errors or asking for help in the <a href=\"https://forums.raspberrypi.com\">Raspberry Pi Forums</a>."
    else
      text+=$'\n'"Support is available on <a href=\"https://discord.gg/RXSTvaUvuu\">Discord</a> and <a href=\"https://github.com/Botspot/pi-apps/issues/new/choose\">Github</a>."
    fi
    
    #if the error_caption is empty, display logfile in the scrollable text field
    if [ -z "$error_caption" ];then
      text+=$'\n'"You can view the terminal output below. (scroll down)"
      error_caption="$(cat "$logfile")"
    else
      text+=$'\n'"Below, Pi-Apps explains what went wrong and how you can fix it."
    fi
    
    #If system is unsupported, display the reason at the top of the scrollable text
    if [ "$supported" == no ];then
      error_caption="$(echo "$unsupported_message" | sed "s/\x1b\[[0-9;]*[a-zA-Z]//g")"$'\n'$'\n'"$error_caption"
    fi
    
    #this dialog may be one in a series of diagnosis dialogs. Name the window-close button accordingly.
    if [ $i -lt $num_lines ];then
      local close_button=(--button="Next error!${DIRECTORY}/icons/forward.png":1)
    else
      local close_button=(--button="Close!${DIRECTORY}/icons/exit.png":1)
    fi
    
    echo "$error_caption" | yad "${yadflags[@]}" --class Pi-Apps --name "Pi-Apps" --text-info --width=700 --height=300 --title="Error occured when $(echo "${action}ing" | sed 's/updateing/updating/g') $app ($i/$num_lines)" \
      --image="${DIRECTORY}/icons/error.png" --image-on-top \
      --text="$text" --wrap --fontname=12 \
      --button='View log'!"${DIRECTORY}/icons/log-file.png"!"Review the output from when <b>$app</b> tried to $action.":"bash -c 'view_file "\""$logfile"\""'" \
      "${send_button[@]}" \
      --button="Retry!${DIRECTORY}/icons/refresh.png"!"Try $(echo "${action}ing" | sed 's/updateing/updating/g') $app again.":3 \
      "${close_button[@]}" >/dev/null
    button="${PIPESTATUS[1]}"
    
    if [ $button == 2 ];then
      #send error log
      send_error_report "$logfile"
    elif [ $button == 3 ];then
      #retry button clicked, return new queue line(s) back to manage daemon
      echo "$line"
    fi
    
    i=$((i+1))
  done
}
#end of logfile functions

#miscellaneous low-level functions below
runonce() { #run command only if it's never been run before. Useful for one-time migration or setting changes.
  #Runs a script in the form of stdin
  
  script="$(< /dev/stdin)"
  
  runonce_hash="$(sha1sum <<<"$script" | awk '{print $1}')"
  
  if [ -s "${DIRECTORY}/data/runonce_hashes" ] && while read line; do [[ $line == "$runonce_hash" ]] && break; done < "${DIRECTORY}/data/runonce_hashes"; then
    #hash found
    #echo "runonce: '$script' already run before. Skipping."
    true
  else
    #run the script.
    bash <(echo "$script")
    #if it succeeds, add the hash to the list to never run it again
    if [ $? == 0 ];then
      echo "$runonce_hash" >> "${DIRECTORY}/data/runonce_hashes"
      echo "runonce(): '$script' succeeded. Added to list."
    else
      echo "runonce(): '$script' failed. Not adding hash to list."
    fi
    
  fi
  
}

text_editor() { #Open user-preferred text editor. $1 is file to open
  [ -z "$1" ] && error "text_editor(): no file specified"
  
  #find the best text editor
  preferrededitor="$(cat "${DIRECTORY}/data/settings/Preferred text editor")"
  # map friendly name of editors to binary name
  if [ "$preferrededitor" == "Visual Studio Code" ];then
    preferrededitor="code"
  elif [ "$preferrededitor" == "VSCodium" ];then
    preferrededitor="codium"
  fi
  
  #change preferred editor if user-default doesn't exist
  if ! command -v "$preferrededitor" >/dev/null;then
    preferrededitor=geany
  fi
  if ! command -v "$preferrededitor" >/dev/null;then
    preferrededitor=mousepad
  fi
  if ! command -v "$preferrededitor" >/dev/null;then
    preferrededitor=leafpad
  fi
  if ! command -v "$preferrededitor" >/dev/null;then
    preferrededitor=nano
  fi
  
  if [ "$preferrededitor" == nano ];then
    #terminal-based text editor
    "${DIRECTORY}/etc/terminal-run" "nano "\""$1"\""" "Editing $(basename "$1")"
  else
    #non-terminal text editor
    (unset GTK_THEME ; unset GDK_BACKEND; "$preferrededitor" "$1")
  fi
}

view_file() { #maximized yad window to view a text file
  local file="$1"
  [ -z "$file" ] && error "view_file(): no input file specified!"
  cat "$file" | yad --center --window-icon="${DIRECTORY}/icons/logo.png" \
    --title="Viewing file" --text="File location: <b>$file</b>" \
    --text-info --tail --maximized \
    --button=Close #--back='#505050' --fore='#00FFFF'
}

files_match() { #$1 and $2 are paths to files
  if [ ! -f "$1" ] || [ ! -f "$2" ];then
    return 1
  else
    diff "$1" "$2" -q >/dev/null
  fi
}

is_supported_system() { #return 0 if system is supported, otherwise return 1
  local PRETTY_NAME="$(cat /etc/os-release | grep PRETTY_NAME | tr -d '"' | awk -F= '{print $2}')"
  local AVAILABLE_REPOS="$(apt-get indextargets --no-release-info --format '$(SITE) $(RELEASE) $(COMPONENT) $(TARGET_OF)' | sort -u | awk '{if ($4=="deb") print $1" "$2" "$3 }' | grep -v '$(COMPONENT)$')"
  local DEFAULT_REPOS="$(echo "$AVAILABLE_REPOS" | grep "raspbian.raspberrypi.org/raspbian\|archive.raspberrypi.org/debian\|\
raspbian.raspberrypi.com/raspbian\|archive.raspberrypi.com/debian\|\
debian.org/debian\|security.debian.org/\|\
ports.ubuntu.com\|esm.ubuntu.com/apps/ubuntu\|esm.ubuntu.com/infra/ubuntu\|\
repo.huaweicloud.com/debian\|repo.huaweicloud.com/ubuntu-ports\|\
deb-multimedia.org|\
apt.pop-os.org\|\
apt.armbian.com")"
  if uname -m | grep -qi 'x86\|i686\|i386'; then
    echo "Pi-Apps is not supported on x86 processors. Nearly all apps will fail. Consider switching to this x86 port of Pi-Apps: https://github.com/MCRaspRBX/pi-apps-x86"
    return 1
  elif grep -q '^/data/media .*Android' /proc/mounts || cat /proc/version | grep -qi Android || cat /proc/version | grep -qi termux || [[ -d /system/app/ && -d /system/priv-app ]]; then
    echo "Pi-Apps is not supported on Android. Some apps will work, but others won't."
    return 1
  elif cat /proc/version | grep -qi Microsoft || cat /proc/sys/kernel/osrelease | grep -qi WSL || [[ -f "/run/WSL" ]] || [[ -f "/etc/wsl.conf" ]] || [ -n "$WSL_DISTRO_NAME" ]; then
    echo "Pi-Apps is not supported on WSL."
    return 1
  elif [ "$(date --help 2>&1 | head -n1 | awk '{print $1}')" == BusyBox ] || [ "$(ps --help 2>&1 | head -n1 | awk '{print $1}')" == BusyBox ];then
    datecommand="$(command -v date)"
    pscommand="$(command -v ps)"
    if [ "$datecommand" != /usr/bin/date ] || [ "$pscommand" != /usr/bin/ps ];then
      echo "Your system has BusyBox commands overriding your main distro's commands. The BusyBox versions of ps, grep, date, and many other commands are missing options that Pi-Apps relies on.
You must fix this issue. Take a look at the directory the commands are stored in, and either remove it or rename it: $(echo "$(dirname "$datecommand" ; dirname "$pscommand")" | sort -u)"
      return 1
    else
      echo "Your system has BusyBox commands in place of the expected linux commands. ps, grep, date, and many other commands are missing options that Pi-Apps relies on.
You must fix this problem before Pi-Apps can function correctly."
      return 1
    fi
  elif ([ "$__os_id" == "Debian" ] || [ "$__os_id" == "Raspbian" ]) && [ -f /etc/rpi-issue ] && [ "$__os_release" == 10 ]; then
    echo "Pi-Apps is no longer supported on your Pi OS ${__os_codename^} operating system. Consider installing Pi OS Bookworm. https://www.raspberrypi.com/news/bookworm-the-new-version-of-raspberry-pi-os/"
    return 1
  elif ([ "$__os_id" == "Debian" ] || [ "$__os_id" == "Raspbian" ]) && [ "$__os_release" -lt 11 ]; then
    echo "Pi-Apps is not supported on your outdated ${__os_id^} ${__os_codename^} operating system. Expect many apps to fail. Consider installing a newer operating system."
    return 1
  elif [ "$__os_id" == "Ubuntu" ] && [ "$__os_release" == "18.04" ] && [ -f /etc/switchroot_version.conf ]; then
    echo "Pi-Apps is no longer supported on your outdated Switchroot ${__os_id^} ${__os_codename^} operating system. Consider installing Switchroot Ubuntu Noble. https://wiki.switchroot.org/wiki/linux/l4t-ubuntu-noble-installation-guide"
    return 1
  elif [ "$__os_id" == "Ubuntu" ] && ! printf '%s\n' "20.04" "$__os_release" | sort -CV ; then
    echo "Pi-Apps is not supported on your outdated ${__os_id^} ${__os_codename^} operating system. Expect many apps to fail. Consider installing a newer operating system."
    return 1
  elif echo "$PRETTY_NAME" | grep -qi 'manjaro'; then
    echo "Pi-Apps is not supported on Manjaro."
    return 1
  elif [[ "$(uname -m)" == armv6* ]]; then
    echo "Pi-Apps is not supported on ARMv6 Raspberry Pi boards. Expect some apps to fail."
    return 1
  elif [ "$(id -u)" == 0 ]; then
    echo "Pi-Apps is not designed to be run as root user."
    return 1
  elif local frankendebian="$(echo "$DEFAULT_REPOS" | grep -v $__os_codename)" && [[ "$__os_desc" != Parrot* ]] && [ ! -z "$frankendebian" ];then
    echo "Congratulations, Linux tinkerer, you broke your system. You have made your system a FrankenDebian.
This website explains your mistake in more detail: https://wiki.debian.org/DontBreakDebian
Your current reported release (${__os_codename^}) should not be combined with other releases.
Specifically, the issue is $(wc -l <<<"$frankendebian" | grep -q 1 && echo 'this line' || echo 'these lines'):"
    local IFS=$'\n'
    for line in $frankendebian ;do
      local site="$(echo "$line" | awk '{print $1}')"
      local release="$(echo "$line" | awk '{print $2}')"
      echo -e "\e[4m$line\e[24m in $(apt-get indextargets --no-release-info --format '$(SOURCESENTRY)' "Release: $release" "Site: $site" | awk -F':' '{print $1}' | sort -u)"
    done
    echo "Your system might be recoverable if you did this recently and have not performed an apt upgrade yet, but otherwise you should probably reinstall your OS."
    return 1
  elif ! package_available init; then
    echo "Congratulations, Linux tinkerer, you broke your system. The init package can not be found, which means you have removed the default debian sources from your system.
All apt based application installs will fail. Unless you have a backup of your /etc/apt/sources.list /etc/apt/sources.list.d you will need to reinstall your OS."
    return 1
  elif [ -z "$AVAILABLE_REPOS" ];then
    echo "Congratulations, Linux tinkerer, you broke your system. You have removed ALL debian sources from your system.
All apt based application installs will fail. Unless you have a backup of your /etc/apt/sources.list /etc/apt/sources.list.d you will need to reinstall your OS."
  # Debian, Ubuntu, and Raspbian should not have an __os_original_id variable set (since there is no upstream repo) but check anyway just incase
  elif ([ ! -z "$__os_original_id" ] && ! ([ "$__os_original_id" == "Debian" ] || [ "$__os_original_id" == "Raspbian" ] || [ "$__os_original_id" == "Ubuntu" ]));then
    echo "Pi-Apps is not supported on $__os_original_desc, Pi-Apps is only officially supported on the latest two LTS releases of Raspberry Pi OS, Raspbian, Debian, and Ubuntu."
    return 1
  elif ([ -z "$__os_original_id" ] && ! ([ "$__os_id" == "Debian" ] || [ "$__os_id" == "Raspbian" ] || [ "$__os_id" == "Ubuntu" ]));then
    echo "Pi-Apps is not supported on $__os_desc, Pi-Apps is only officially supported on the latest two LTS releases of Raspberry Pi OS, Raspbian, Debian, and Ubuntu."
    return 1
  elif [ "$__os_id" == "Ubuntu" ] && ! ( echo "$DEFAULT_REPOS" | grep "$__os_codename " | awk '{if ($3=="main" || $3=="universe") print $3 }' | sort -u | tr '\n' ' ' | grep -q "main universe" && \
    echo "$DEFAULT_REPOS" | grep "$__os_codename-updates " | awk '{if ($3=="main" || $3=="universe") print $3 }' | sort -u | tr '\n' ' ' | grep -q "main universe" && \
    echo "$DEFAULT_REPOS" | grep "$__os_codename-security " | awk '{if ($3=="main" || $3=="universe") print $3 }' | sort -u | tr '\n' ' ' | grep -q "main universe" ); then
    echo "MISSING Default Ubuntu Repositories!
Pi-Apps does NOT support systems without ALL of $__os_codename, $__os_codename-updates, and $__os_codename-security dists and main and universe components present in the sources.list
Please refer to the default sources.list for Ubuntu and restore all required dists and components."
    return 1
  elif [ "$__os_id" == "Debian" ] && ! ( echo "$DEFAULT_REPOS" | grep "debian.org/debian" | grep "$__os_codename " | awk '{if ($3=="main") print $3 }' | sort -u | tr '\n' ' ' | grep -q "main" && \
    echo "$DEFAULT_REPOS" | grep "debian.org/debian" | grep "$__os_codename-updates " | awk '{if ($3=="main") print $3 }' | sort -u | tr '\n' ' ' | grep -q "main" && \
    echo "$DEFAULT_REPOS" | grep "debian.org/debian" | grep "$__os_codename-security " | awk '{if ($3=="main") print $3 }' | sort -u | tr '\n' ' ' | grep -q "main" ); then
    echo "MISSING Default Debian Repositories!
Pi-Apps does NOT support systems without ALL of $__os_codename, $__os_codename-updates, and $__os_codename-security dists and main component present in the sources.list
Please refer to the default sources.list for Debian and restore all required dists and components."
    return 1
  elif [ "$__os_id" == "Raspbian" ] && ! ( echo "$DEFAULT_REPOS" | grep "/raspbian" | grep "$__os_codename " | awk '{if ($3=="main") print $3 }' | sort -u | tr '\n' ' ' | grep -q "main" ); then
    echo "MISSING Default Raspbian Repositories!
Pi-Apps does NOT support systems without $__os_codename dist and main component present in the sources.list
Please refer to the default sources.list for Raspbian and restore all required dists and components."
    return 1
  elif ! apt-get --dry-run check &>/dev/null ; then
    echo "Congratulations, Linux tinkerer, you broke your system. There are packages on your system that are in a broken state.
Refer to the output below for any potential solutions.

$(apt-get --dry-run check)"
    return 1
  elif [ "$(df -a / -B 1 --output=avail | tail -1 | tr -d ' ')" -lt $((500*1024*1024)) ];then
    echo "Your system drive has less than 500MB of free space. Watch out for "\""disk full"\"" errors."
    return 1
  else
    return 0
  fi
}

get_device_info() { #returns information about current install and hardware
  echo "OS: $(cat /etc/os-release | grep PRETTY_NAME | tr -d '"' | awk -F= '{print $2}')"
  echo "OS architecture: ${arch}-bit"
  #### IMPORTANT: for anyone wishing to change the wording following this, you MUST also change the send_error_report function
  [ ! -z "$DIRECTORY" ] && echo "Last updated Pi-Apps on: $(cd "$DIRECTORY"; git show -s --format="%ad" --date=short | xargs date +%x -d)"
  [ -s "$DIRECTORY/etc/git_url" ] && echo "Latest Pi-Apps version: $(wget -T 3 https://api.github.com/repos/Botspot/pi-apps/commits/master -qO- 2>&1 | grep '"date":' | tail -n 1 | sed 's/"date"://g' | xargs date +%x -d)"
  echo "Kernel: $(uname -m) $(uname -r)"
  #obtain model and SOC_ID
  get_model
  echo "Device model: $model"
  [[ ! -z "$SOC_ID" ]] && echo "SOC identifier: $SOC_ID"
  # obtain (hashed) machine_id (only if file exists and has contents)
  [ -s /etc/machine-id ] && echo "Machine-id (hashed): $(cat /etc/machine-id | sha1sum | awk '{print $1}' | head -1)"
  # obtain (hashed) serial_number (only if file exists and has contents)
  [ -s /sys/firmware/devicetree/base/serial-number ] && echo "Serial-number (hashed): $(cat /sys/firmware/devicetree/base/serial-number | sha1sum | awk '{print $1}' | head -1)"
  echo "Cpu name: $( lscpu | awk '/Model name:/ {print $3}' )"
  echo "Ram size: $(echo "scale=2 ; $( awk '/MemTotal/ {print $2}' /proc/meminfo ) / 1024000 " | bc ) GB"
  
  if [ -f /etc/rpi-issue ];then
    echo "Raspberry Pi OS image version: $(cat /etc/rpi-issue | grep 'Raspberry Pi reference' | sed 's/Raspberry Pi reference //g')"
  fi
  
  if [ ! -z "$LANG" ];then
    echo "Language: $LANG"
  elif [ ! -z "$LC_ALL" ];then
    echo "Language: $LC_ALL"
  fi
  
}

get_model() { # populates the model and jetson_model variables with information about the current hardware
  # obtain model name
  unset model
  # typical linux arm model name location
  if [[ -z "$model" ]] && [[ -f /sys/firmware/devicetree/base/model ]]; then
    model="$(tr -d '\0' < /sys/firmware/devicetree/base/model)"
  fi
  # linux model name location for some oracle machines
  if [[ -z "$model" ]] && [[ -f /sys/firmware/devicetree/base/banner-name ]]; then
    model="$(tr -d '\0' < /sys/firmware/devicetree/base/banner-name)"
  fi
  # linux model name location for some embedded systems
  if [[ -z "$model" ]] && [[ -f /tmp/sysinfo/model ]]; then
    model="$(tr -d '\0' < /tmp/sysinfo/model)"
  fi
  # typical linux x86 model name locations
  if [[ -z "$model" ]] && [[ -f /sys/devices/virtual/dmi/id/product_name ]]; then
    model="$(tr -d '\0' < /sys/devices/virtual/dmi/id/product_name)"
  fi
  if [[ -z "$model" ]] && [[ -f /sys/class/dmi/id/product_name ]]; then
    model="$(tr -d '\0' < /sys/class/dmi/id/product_name)"
  fi
  # typical android container model name location
  if [[ -z "$model" ]] && [[ -d /system/app/ && -d /system/priv-app ]]; then
    model="$(getprop ro.product.marketname)"
    if [[ -z "$model" ]]; then
      model="$(getprop ro.vendor.product.display)"
    fi
    if [[ -z "$model" ]]; then
      model="$(getprop ro.config.devicename)"
    fi
    if [[ -z "$model" ]]; then
      model="$(getprop ro.config.marketing_name)"
    fi
    if [[ -z "$model" ]]; then
      model="$(getprop ro.product.vendor.model)"
    fi
    if [[ -z "$model" ]]; then
      model="$(getprop ro.product.oppo_model)"
    fi
    if [[ -z "$model" ]]; then
      model="$(getprop ro.oppo.market.name)"
    fi
    if [[ -z "$model" ]]; then
      model="$(getprop ro.product.model)"
    fi
    if [[ -z "$model" ]]; then
      model="$(getprop ro.product.product.model)"
    fi
    if [[ -z "$model" ]]; then
      model="$(getprop ro.product.odm.model)"
    fi
  fi
  unset jetson_model
  unset SOC_ID
  # obtain jetson model name (if available)
  # nvidia, in their official L4T (Linux for Tegra) releases 32.X and 34.X, set a distinct tegra family in the device tree /proc/device-tree/compatible
  if [[ -e "/proc/device-tree/compatible" ]]; then
    CHIP="$(tr -d '\0' < /proc/device-tree/compatible)"
    if [[ ${CHIP} =~ "tegra20" ]]; then
      jetson_model="tegra-2"
    elif [[ ${CHIP} =~ "tegra30" ]]; then
      jetson_model="tegra-3"
    elif [[ ${CHIP} =~ "tegra114" ]]; then
      jetson_model="tegra-4"
    elif [[ ${CHIP} =~ "tegra124" ]]; then
      jetson_model="tegra-k1-32"
    elif [[ ${CHIP} =~ "tegra132" ]]; then
      jetson_model="tegra-k1-64"
    elif [[ ${CHIP} =~ "tegra210" ]]; then
      jetson_model="tegra-x1"
    elif [[ ${CHIP} =~ "tegra186" ]]; then
      jetson_model="tegra-x2"
    elif [[ ${CHIP} =~ "tegra194" ]]; then
      jetson_model="xavier"
    elif [[ ${CHIP} =~ "tegra234" ]]; then
      jetson_model="orin"
    elif [[ ${CHIP} =~ "tegra239" ]]; then
      jetson_model="switch-pro-chip"
    elif [[ ${CHIP} =~ "tegra" ]]; then
      jetson_model="jetson-unknown"
    elif [[ ${CHIP} =~ "rk3399" ]]; then
      SOC_ID="rk3399"
    elif [[ ${CHIP} =~ "rk3308" ]]; then
      SOC_ID="rk3308"
    elif [[ ${CHIP} =~ "rk3326" ]]; then
      SOC_ID="rk3326"
    elif [[ ${CHIP} =~ "rk3328" ]]; then
      SOC_ID="rk3328"
    elif [[ ${CHIP} =~ "rk3368" ]]; then
      SOC_ID="rk3368"
    elif [[ ${CHIP} =~ "rk3566" ]]; then
      SOC_ID="rk3566"
    elif [[ ${CHIP} =~ "rk3568" ]]; then
      SOC_ID="rk3568"
    elif [[ ${CHIP} =~ "g12b" ]]; then
      SOC_ID="g12b"
    elif [[ ${CHIP} =~ "g12b" ]]; then
      SOC_ID="g12b"
    elif [[ ${CHIP} =~ "g12b" ]]; then
      SOC_ID="g12b"
    elif [[ ${CHIP} =~ "bcm2712" ]]; then
      SOC_ID="bcm2712"
    elif [[ ${CHIP} =~ "bcm2711" ]]; then
      SOC_ID="bcm2711"
    elif [[ ${CHIP} =~ "bcm2837" ]]; then
      SOC_ID="bcm2837"
    elif [[ ${CHIP} =~ "bcm2836" ]]; then
      SOC_ID="bcm2836"
    elif [[ ${CHIP} =~ "bcm2835" ]]; then
      SOC_ID="bcm2835"
    fi
  # as part of the 2X.X L4T releases, the kernel is older and the tegra family is found in /sys/devices/soc0/family
  elif [[ -e "/sys/devices/soc0/family" ]]; then
    CHIP="$(tr -d '\0' < /sys/devices/soc0/family)"
    if [[ ${CHIP} =~ "tegra20" ]]; then
      jetson_model="tegra-2"
    elif [[ ${CHIP} =~ "tegra30" ]]; then
      jetson_model="tegra-3"
    elif [[ ${CHIP} =~ "tegra114" ]]; then
      jetson_model="tegra-4"
    elif [[ ${CHIP} =~ "tegra124" ]]; then
      jetson_model="tegra-k1-32"
    elif [[ ${CHIP} =~ "tegra132" ]]; then
      jetson_model="tegra-k1-64"
    elif [[ ${CHIP} =~ "tegra210" ]]; then
      jetson_model="tegra-x1"
    fi
  fi
  if [ -n "$jetson_model" ]; then
    SOC_ID="$jetson_model"
  fi
}

get_codename() { #get debian/ubuntu codename
  if ! command -v lsb_release >/dev/null; then
    apt_update &>/dev/null && \
    sudo apt-get install -y lsb-release &>/dev/null
  fi
  
  # first check if lsb_release has an upstream option -u
  # if not, check if there is an upstream-release file
  # if not, check if there is a lsb-release.diverted file
  # if not, assume that this is not a ubuntu derivative
  if lsb_release -a -u &>/dev/null; then
    # This is a Ubuntu Derivative, checking the upstream-release version info
    lsb_release -s -c -u
  elif [ -f /etc/upstream-release/lsb-release ]; then
    # ubuntu 22.04+ linux mint no longer includes the lsb_release -u option
    # add a parser for the /etc/upstream-release/lsb-release file
    source /etc/upstream-release/lsb-release
    echo "$DISTRIB_CODENAME"
    unset DISTRIB_ID DISTRIB_DESCRIPTION DISTRIB_RELEASE DISTRIB_CODENAME
  elif [ -f /etc/lsb-release.diverted ]; then
    # ubuntu 22.04+ popOS no longer includes the /etc/upstream-release/lsb-release or the lsb_release -u option
    # add a parser for the new /etc/lsb-release.diverted file
    source /etc/lsb-release.diverted
    echo "$DISTRIB_CODENAME"
    unset DISTRIB_ID DISTRIB_DESCRIPTION DISTRIB_RELEASE DISTRIB_CODENAME
  else
    lsb_release -s -c
  fi
}

multi_install_gui() { #graphical interface to install multiple apps from a list.
  
  #get list of apps - hide hidden apps and hide installed apps
  local apps="$(list_apps cpu_installable | list_subtract "$(list_apps hidden)" | list_subtract "$(list_apps installed)")"
  
  local IFS=$'\n'
  local app
  
  apps="$(for app in $apps ;do
    echo "FALSE
${DIRECTORY}/apps/$app/icon-24.png
$app
$(head -n1 "${DIRECTORY}/apps/$app/description")"
  done | sed 's/&/&amp;/g' | yad "${yadflags[@]}" --window-icon="${DIRECTORY}/icons/settings.png" --list --checklist \
    --width=300 --height=500 \
    --hide-column=4 --print-column=3 --tooltip-column=4 --no-headers \
    --text="Install everything you want!"$'\n'"Note: apps that are already installed are not shown." \
    --column=:chk --column=:img --column=name --column=:tip \
    --button=Cancel!"${DIRECTORY}/icons/exit.png":1 --button='Install selected'!"${DIRECTORY}/icons/install.png":0)"
  
  if [ $? == 0 ] && [ ! -z "$apps" ];then
    #remove empty lines from yad's output
    apps="$(grep . <<<"$apps")"
    
    for app in $apps ;do
      queue+="install $app"$'\n'
    done
    queue="${queue::-1}" #remove final newline character
    terminal_manage_multi "$queue" &
  fi
}

multi_uninstall_gui() { #graphical interface to uninstall multiple apps from a list.
  
  #get list of apps that are installed
  local apps="$(list_apps installed)"
  
  local IFS=$'\n'
  local app
  
  apps="$(for app in $apps ;do
    echo "FALSE
${DIRECTORY}/apps/$app/icon-24.png
$app
$(head -n1 "${DIRECTORY}/apps/$app/description")"
  done | sed 's/&/&amp;/g' | yad "${yadflags[@]}" --window-icon="${DIRECTORY}/icons/settings.png" --list --checklist \
    --width=300 --height=500 \
    --hide-column=4 --print-column=3 --tooltip-column=4 --no-headers \
    --text="Uninstall everything you want!"$'\n'"Note: apps that are not installed are not shown." \
    --column=:chk --column=:img --column=name --column=:tip \
    --button=Cancel!"${DIRECTORY}/icons/exit.png":1 --button='Uninstall selected'!"${DIRECTORY}/icons/uninstall.png":0)"
  
  if [ $? == 0 ] && [ ! -z "$apps" ];then
    #remove empty lines from yad's output
    apps="$(grep . <<<"$apps")"
    
    for app in $apps ;do
      queue+="uninstall $app"$'\n'
    done
    queue="${queue::-1}" #remove final newline character
    terminal_manage_multi "$queue" &
  fi
}

process_exists() { #return 0 if the $1 PID is running, otherwise 1
  [ -z "$1" ] && error "process_exists(): no PID given!"
  
  if [ -f "/proc/$1/status" ];then
    return 0
  else
    return 1
  fi
}
#end of low-level functions

#command interceptors - functions that enhance a command
enable_module() { #Permanent equivalent to modprobe, this will load the module on every future startup
  local module="$1"
  [ -z "$module" ] && error "enable_module(): The name of a kernel module must be specified!"
  
  #This function is often to load the "fuse" module.
  # Fuse is most often used for AppImages.
  #   AppImages need libfuse2 to be installed.
  #    Install libfuse2 to avoid AppImage launch failures.
  if [ "$module" == 'fuse' ];then
    if [ ! -z "$app" ];then
      #this function is being used within an app-installation. Make libfuse2 a dependency, whether or not it is already installed.
      if package_available fuse3 ;then
        install_packages fuse3 libfuse2
      elif package_available fuse ;then
        install_packages fuse libfuse2
      else
        # this case should never be reached
        install_packages libfuse2
      fi
    elif package_installed libfuse2 && (package_installed fuse || package_installed fuse3) ;then
      #NOT being used within an app-installation and libfuse2 and fuse or fuse3 is already installed.
      true #nothing to do
    else
      #NOT being used within an app-installation and libfuse2 is not installed.
      apt_update
      if package_available fuse3 ;then
        sudo apt install -y fuse3 libfuse2 --no-install-recommends
      elif package_available fuse ;then
        sudo apt install -y fuse libfuse2 --no-install-recommends
      else
        # this case should never be reached
        sudo apt install -y libfuse2 --no-install-recommends
      fi
    fi
  fi

  if ! package_installed kmod ;then
    if [ ! -z "$app" ];then
      #this function is being used within an app-installation. Make kmod a dependency since it is not installed.
      install_packages kmod
      hash -r
    else
      #NOT being used within an app-installation and kmod is not installed.
      apt_update
      sudo apt install -y kmod --no-install-recommends
      hash -r
    fi
  fi
  
  # if module is builtin, skip remaining checks
  # this check is not foolproof on all kernels so some may still have the module builtin and fail the below check
  if [[ "$(modinfo --filename "$module")" == "(builtin)" ]]; then
    return 0
  fi

  #load the module now if not already loaded
  #all active kernel modules have a folder in /sys/module matching the module name
  if [[ ! -d "/sys/module/$module" ]]; then
    errors="$(sudo modprobe "$module" 2>&1)"
    if [ $? != 0 ];then
      #if modprobe fails, the module may be missing because user upgraded the kernel and has not rebooted yet.
      if [ ! -d "/lib/modules/$(uname -r)" ];then
        error "\nUser error: Failed to load the '$module' kernel module because you upgraded the kernel and have not rebooted yet.
Please reboot to load the new kernel, then try again."
      else
        #other modprobe error: exit now and display modprobe output
        error "$errors"
      fi
    fi
  fi
  
  #make it load on boot if system supports loading modules
  if [ -f /proc/modules ] && [ ! -f "/etc/modules-load.d/${module}.conf" ];then
    echo "$module" | sudo tee "/etc/modules-load.d/${module}.conf" >/dev/null
  fi
}

git_clone() { #silently clone a git repository but display the output if an error occurs
  # $1 is repo
  local IFS=' '
  local arg
  local prevarg
  local repo_name=''
  for arg in "$@"; do
    if [[ "$arg" == *'://'* ]];then
      local url="$arg"
      repo_name=$(basename "$url" | sed s/.git//g)
    elif [ ! -z "$repo_name" ] && [[ "$arg" != -* ]] && [[ "$prevarg" != -* ]];then #repo_name already set, so this arg is after the url to specify the download folder name
      repo_name="$arg"
    fi
    prevarg="$arg"
  done
  unset prevarg
  
  [ -z "$url" ] && error "git_clone(): no repository URL specified."
  
  local folder="$(pwd)/$repo_name"
  
  status -n "Downloading $repo_name repository... "
  
  rm -rf "$repo_name" || sudo rm -rf "$repo_name"
  local errors
  errors="$(git clone "$@" 2>&1)"
  local exitcode=$?
  
  if [ "$exitcode" != 0 ]; then
    error "\nFailed to download $repo_name repository.\nErrors: $errors"
  fi
  
  status_green 'Done'
}

wget() { #Intercept all wget commands. When possible, uses aria2c.
  # FIXME: use wget always until upstream bug is resolved https://github.com/aria2/aria2/issues/2197
  local file=''
  local url=''
  local passopts=()
  #determine the download manager to use
  local use=wget
  #determine if being run silently (if the '-q' flag was passed)
  local quiet=0
  
  #convert wget arguments to newline-separated list
  local IFS=$'\n'
  local opts="$(IFS=$'\n'; echo "$*")"
  for opt in $opts ;do
    
    #check if this argument to wget begins with '--'
    if [[ "$opt" == '--'* ]];then
      if [ "$opt" == '--quiet' ];then
        quiet=1
      elif [[ "$opt" == '--header='* ]];then
        passopts+=("$opt")
      else #for any other arguments, fallback to wget
        use=wget
      fi
      
    elif [ "$opt" == '-' ];then
      #writing to stdout, use wget and hide output
      use=wget
      quiet=1
    elif [[ "$opt" == '-'* ]];then
      #this opt is a flag beginning with one '-'
      
      #check the value of every letter in this argument
      local i
      for i in $(fold -w1 <<<"$opt" | tail -n +2) ;do
        
        if [ "$i" == q ];then
          quiet=1
        elif [ "$i" == O ];then
          true
        elif [ "$i" == '-' ];then
          #writing to stdout, use wget and hide output
          use=wget
          quiet=1
        else #any other wget arguments
          use=wget
        fi
      done
      
    elif [[ "$opt" == *'://'* ]]; then
      #this opt is web address
      url="$opt"
    elif [[ "$opt" == '/'* ]]; then
      #this opt is file output
      if [ -z "$file" ];then
        file="$opt"
        #if output file is /dev/stdout, /dev/null, etc, use wget
        if [[ "$file" == /dev/* ]];then
          use=wget
          quiet=1
        fi
      else #file var already populated
        use=wget
      fi
    else
      #This argument does not begin with '-', contain '://', or begin with '/'.
      #Assume output file specified shorthand if file-argument is not already set
      if [ -z "$file" ];then
        file="$(pwd)/${opt}"
      else #file var already populated
        use=wget
      fi
    fi
  done
  
  if ! command -v aria2c >/dev/null ;then
    #aria2c command not found
    use=wget
  fi
  
  local filename="$(echo "$url" | sed 's+/download$++g' | sed 's+.*/++g')"
  if [ "$quiet" == 0 ];then
    if [ -n "$file" ] && [ "$file" != "$(pwd)/$filename" ]; then
      status -n "Downloading $filename to $file... " 1>&2
    else
      status -n "Downloading $filename... " 1>&2
    fi
    echo
  fi
  
  #now, perform the download using the chosen method
  if [ "$use" == wget ];then
    #run the true wget binary with all this function's args
    
    command wget --progress=bar:force:noscroll "$@"
    local exitcode=$?
  elif [ "$use" == aria2c ];then
    
    #if $file empty, generate it based on url
    if [ -z "$file" ];then
      file="$(pwd)/$filename"
    fi
    
    #use these flags for aria2c
    aria2_flags=(-x 16 -s 16 --max-tries=10 --retry-wait=30 --max-file-not-found=5 --http-no-cache=true --check-certificate=false \
      --allow-overwrite=true --auto-file-renaming=false --remove-control-file --auto-save-interval=0 \
      --console-log-level=error --show-console-readout=false --summary-interval=1 "$url" -d "$(dirname "${file}")" -o "$(basename "${file}")" "${passopts[@]}")
    
    #suppress output if -q flag passed
    if [ "$quiet" == 1 ];then
      aria2c --quiet "${aria2_flags[@]}"
      local exitcode=$?
      
    else #run aria2c without quietness and format download-progress output
      local terminal_width="$(tput cols || echo 80)"
      
      #run aria2c and reduce its output.
      aria2c "${aria2_flags[@]}" | while read -r line ;do
        
        #filter out unnecessary lines
        line="$(grep --line-buffered -v '\-\-\-\-\-\-\-\-\|======\|^FILE:\|^$\|Summary\|Results:\|download completed\.\|^Status Legend:\||OK\||stat' <<<"$line" || :)"
        
        if [ ! -z "$line" ];then #if this line still contains something and was not erased by grep
          
          #check if this line is a progress-stat line, like: "[#a6567f 20MiB/1.1GiB(1%) CN:16 DL:14MiB ETA:1m19s]"
          if [[ "$line" == '['*']' ]];then
            
            #hide cursor
            printf "\e[?25l"
            
            #print the total data only, like: "0.9GiB/1.1GiB"
            statsline="$(echo "$line" | awk '{print $2}' | sed 's/(.*//g' | tr -d '\n') "
            #get the length of statsline
            characters_subtract=${#statsline}
            
            #determine how many characters are available for the progress bar
            available_width=$(($terminal_width - $characters_subtract))
            #make sure available_width is a positove number (in case bash-variable COLUMNS is empty)
            [ "$available_width" -le 0 ] && available_width=20
            
            #get progress percentage from aria2c output
            percent="$(grep -o '(.*)' <<<"$line" | tr -d '()%')"
            
            #echo "percent: $percent"
            #echo "available_width: $available_width"
            
            #determine how many characters in progress bar to light up
            progress_characters=$(((percent*available_width)/100))
            
            statsline+="\e[92m\e[1m$(for ((i=0; i<$progress_characters; i++)); do printf "—"; done)\e[39m" # other possible characters to put here: █🭸
            echo -ne "\e[0K${statsline}\r\e\e[0m" 1>&2 #clear and print over previous line
            
            #reduce the line and print over the previous line, like: "1.1GiB/1.1GiB(98%) DL:18MiB"
            #echo "$line" | awk '{print $2 " " $4 " " substr($5, 1, length($5)-1)}' | tr -d '\n'
            
          else
            #this line is not a progress-stat line; don't format output
            echo "$line"
          fi
        fi
        
      done
      local exitcode=${PIPESTATUS[0]}
    fi
  fi
  
  #display a "download complete" message
  if [ $exitcode == 0 ] && [ "$quiet" == 0 ];then
    
    #show cursor
    printf "\e[?25h"
    
    #display "done" message
    if [ "$use" == aria2c ];then
      local progress_characters=$(($terminal_width - 5))
      echo -e "\e[0KDone \e[92m\e[1m$(for ((i=0; i<$progress_characters; i++)); do printf "—"; done)\e[39m\e[0m" 1>&2 #clear and print over previous line
    else
      echo
      status_green "Done" 1>&2
    fi
  elif [ $exitcode != 0 ] && [ "$quiet" == 0 ];then
    #show cursor
    printf "\e[?25h"
    
    echo -e "\n\e[91mFailed to download: $url\nPlease review errors above.\e[0m" 1>&2
  fi
  
  return $exitcode
}

chmod() { #say what is being made executable
  status "Making executable: $2"
  command chmod "$@"
  return $?
}

unzip() { #say what is being extracted
  #some scripts add a flag to the unzip command before specifying the file.
  #This checks the first two arguments to display the file being extracted.
  [ -f "$1" ] && status "Extracting: $1"
  [ -f "$2" ] && status "Extracting: $2"
  
  #The -o flag means to overwrite without prompting
  command unzip -o "$@"
  return $?
}

sudo_popup() { #just like sudo on passwordless systems like PiOS, but displays a password dialog otherwise. Avoids displaying a password prompt to an invisible terminal.
  if sudo -n true; then
    # sudo is available (within sudo timer) or passwordless
    sudo "$@"
  else
    # sudo is not available (not within sudo timer)
    pkexec "$@"
  fi
}

nproc() { #Reduce the number of compile threads on low-RAM systems
  #The Pi02 and Pi3A+ have 4 cores but 500MB of RAM. This function reduces the number of threads to use if RAM is low.
  
  #Estimation of how much memory is available for starting new applications, without swapping.
  local available="$(free | grep 'Mem:' | awk '{print $7}')"
  
  if [ "$available" -gt $((2000*1024)) ] || [ "$GITHUB_ACTIONS" == "true" ];then
    #available memory > 2000MB, use normal number of threads
    command nproc
  elif [ "$available" -gt $((1500*1024)) ];then
    #1500MB < available memory <= 2000MB, use 3 threads
    echo 3
    warning "Your system has less than 2000MB of available RAM, so this will compile with only 3 threads."
  elif [ "$available" -gt $((1000*1024)) ];then
    #1000MB < available memory <= 1500MB, use 2 threads
    echo 2
    warning "Your system has less than 1500MB of available RAM, so this will compile with only 2 threads."
  else
    #available memory <= 1000MB, use 1 thread
    echo 1
    warning "Your system has less than 1000MB of available RAM, so this will compile with only 1 thread."
  fi
}
#end of command interceptors

#Stop a running api function if user presses Ctrl+C, but avoid doing this if api is sourced in an interactive terminal.
if [[ $- != *i* ]] ;then
  trap "exit 1" INT
fi

add_english

#if this script is being run standalone, set the DIRECTORY variable based on the script's location
if [[ "$0" == */api ]];then
  #it is necessary to set it now in order for yadflags and yad theme to work as intended.
  DIRECTORY="$(readlink -f "$(dirname "$0")")"
  export DIRECTORY
#if being sourced, ensure DIRECTORY variable is set
elif [ -z "$DIRECTORY" ] || [ "$DIRECTORY" == "$HOME" ] || [ ! -d "$DIRECTORY" ] || [ ! -f "${DIRECTORY}/api" ] || [ ! -f "${DIRECTORY}/gui" ];then
  echo "DIRECTORY variable must be set to a valid pi-apps folder. Default folder: $HOME/pi-apps"
  return 1
fi

#set the system GTK theme for yad windows
guimode="$(cat "${DIRECTORY}/data/settings/App List Style" 2>/dev/null || echo yad-default)"
if [ "$guimode" == yad-default ];then
  export GTK_THEME=''
elif [[ "$guimode" = yad* ]];then
  export GTK_THEME=${guimode//yad-/}
elif [ "$guimode" == xlunch-light-3d ];then
  export GTK_THEME=''
elif [ "$guimode" == xlunch-dark-3d ];then
  export GTK_THEME=Adwaita-dark
elif [ "$guimode" == xlunch-dark ];then
  export GTK_THEME=Adwaita-dark
fi

#this array stores flags that are used in all yad windows - saves on the typing and makes it easy to change an attribute on all dialogs from one place.
yadflags=(--class Pi-Apps --name "Pi-Apps" --center --window-icon="${DIRECTORY}/icons/logo.png" --title="Pi-Apps" --separator='\n')

#determine if host system is 64 bit arm64 or 32 bit armhf
if [ "$(od -An -t x1 -j 4 -N 1 "$(readlink -f /sbin/init)")" = ' 02' ];then
  arch=64
elif [ "$(od -An -t x1 -j 4 -N 1 "$(readlink -f /sbin/init)")" = ' 01' ];then
  arch=32
else
  error "Failed to detect OS CPU architecture! Something is very wrong."
fi

#Make dual-pane yad windows work correctly on wayland by using xwayland
#We need support for setting window position for dual-pane yad windows to be supported via a wayland protocol, implemented in compositors, and supported in gtk
#Relevant draft PRs for the wayland protocol https://gitlab.freedesktop.org/wayland/wayland-protocols/-/merge_requests/247 https://gitlab.freedesktop.org/wayland/wayland-protocols/-/merge_requests/249
export GDK_BACKEND=x11

# add upstream and original lsb-release info for all scripts to use

# _os_original_* variables are only set for downstream releases (eg: Pop!_OS, Linux Mint, and KDE Neon)
# this allows for scripts to simply check for the existance of _os_original_* variables and operate differently if found
# note: Pop!_OS, Linux Mint, and KDE Neon are NOT official Ubuntu flavors (like Kubuntu, Ubuntu MATE, Ubuntu Kylin, Ubuntu Budgie, Lubuntu, Ubuntu Studio, Ubuntu Unity, and Xubuntu)
# official Ubuntu flavors use ONLY the official Ubuntu repositories and are directly supported by Ubuntu. These other releases are simply "based" on Ubuntu, much like Raspbian and PiOS are "based" on Debian.

# first check if lsb_release has an upstream option -u
# if not, check if there is an upstream-release file
# if not, check if there is a lsb-release.diverted file
# if not, assume that this is not a ubuntu derivative
# use mapfile to temporarily store release info for speed
if [ -z "$__os_id" ] || [ -z "$__os_desc" ] || [ -z "$__os_release" ] || [ -z "$__os_codename" ]; then
  if lsb_release -a -u &>/dev/null; then
    # This is a Ubuntu Derivative, checking the upstream-release version info
    mapfile -t os_u < <(lsb_release -s -i -d -r -c -u)
    export __os_id="${os_u[0]}"
    export __os_desc="${os_u[1]}"
    export __os_release="${os_u[2]}"
    export __os_codename="${os_u[3]}"
    mapfile -t os < <(lsb_release -s -i -d -r -c)
    export __os_original_id="${os[0]}"
    export __os_original_desc="${os[1]}"
    export __os_original_release="${os[2]}"
    export __os_original_codename="${os[3]}"
  elif [ -f /etc/upstream-release/lsb-release ]; then
    # ubuntu 22.04+ Linux Mint no longer includes the lsb_release -u option
    # add a parser for the /etc/upstream-release/lsb-release file
    source /etc/upstream-release/lsb-release
    export __os_id="$DISTRIB_ID"
    export __os_desc="$DISTRIB_DESCRIPTION"
    export __os_release="$DISTRIB_RELEASE"
    export __os_codename="$DISTRIB_CODENAME"
    mapfile -t os < <(lsb_release -s -i -d -r -c)
    export __os_original_id="${os[0]}"
    export __os_original_desc="${os[1]}"
    export __os_original_release="${os[2]}"
    export __os_original_codename="${os[3]}"
    unset DISTRIB_ID DISTRIB_DESCRIPTION DISTRIB_RELEASE DISTRIB_CODENAME
  elif [ -f /etc/lsb-release.diverted ]; then
    # ubuntu 22.04+ Pop!_OS no longer includes the /etc/upstream-release/lsb-release or the lsb_release -u option
    # add a parser for the new /etc/lsb-release.diverted file
    source /etc/lsb-release.diverted
    export __os_id="$DISTRIB_ID"
    export __os_desc="$DISTRIB_DESCRIPTION"
    export __os_release="$DISTRIB_RELEASE"
    export __os_codename="$DISTRIB_CODENAME"
    mapfile -t os < <(lsb_release -s -i -d -r -c)
    export __os_original_id="${os[0]}"
    export __os_original_desc="${os[1]}"
    export __os_original_release="${os[2]}"
    export __os_original_codename="${os[3]}"
    unset DISTRIB_ID DISTRIB_DESCRIPTION DISTRIB_RELEASE DISTRIB_CODENAME
  else
    mapfile -t os < <(lsb_release -s -i -d -r -c)
    export __os_id="${os[0]}"
    export __os_desc="${os[1]}"
    export __os_release="${os[2]}"
    export __os_codename="${os[3]}"
  fi
fi

# add CPU op-modes for all scripts to use
if ([ -z "$__cpu_op_mode_32" ] && [ -z "$__cpu_op_mode_64" ]) || [ -z "$__cpu_op_modes" ]; then
  if lscpu | grep "CPU op-mode(s):" | grep -q "32-bit, 64-bit"; then
    export __cpu_op_modes="32/64"
    export __cpu_op_mode_32=true
    export __cpu_op_mode_64=true
  elif lscpu | grep "CPU op-mode(s):" | grep -q "32-bit"; then
    export __cpu_op_modes="32"
    export __cpu_op_mode_32=true
  elif lscpu | grep "CPU op-mode(s):" | grep -q "64-bit"; then
    export __cpu_op_modes="64"
    export __cpu_op_mode_64=true
  fi
fi

# add /usr/local/bin to path if not currently present which is often not searched by default if /usr/local/bin did not exist on boot.
# this path will automatically be added on next boot if a binary is placed in it by a pi-apps script
[[ "$PATH" != */usr/local/bin* ]] && export PATH="/usr/local/bin:$PATH"

#if this script is being run standalone, run the specified function
if [[ "$0" == */api ]];then
  "$@"
  exit $?
fi

export DIRECTORY
